Forum Programmation.python Multiprocessing

Posté par  . Licence CC By‑SA.
Étiquettes :
4
23
déc.
2016

Bonjour,

Afin de me former à l’utilisation de la bibliothèque multiprocessing de Python3 j’écris un peu de code, pour mettre en pratique… Il y a des concepts que je ne comprends pas bien, d’où ce post.

Pour mon exemple j’ai imaginé le problème suivant : une crèche, qui possède des nourrissons et des dortoirs, doit faire faire la sieste à tous les nourrissons. La sieste de l’ensemble des nourrissons d’un dortoir est terminée lorsque le dernier des nourrissons a fini sa sieste, les nourrissons dormant un temps aléatoire.

Dans le code suivant, je fais dormir deux fois l’ensemble des nourrissons. Une première fois en utilisant un seul dortoir, donc un seul pool de process, j’utilise la fonction map() du pool. Le temps nécessaire pour faire dormir tous les nourrissons c’est l’addition du temps de sieste de chaque chambrée, vu qu’elles occupe le dortoir l’une après l’autre. C’est le fonctionnement synchrone. Je comprends ce que fait le code, je n’ai pas d’interrogation dans ce cas. Pour faire dormir à nouveau l’ensemble des nourrissons j’emploie maintenant un ensemble de N dortoirs (N = nombre de nourrissons / taille d’un dortoir + 1) et la fonction map_async(), et c’est là que je commence à ne plus trop comprendre ce que je suis en train de faire…

#!/usr/bin/env python3
"""Make all the toddlers in the nursery have their nap."""

from multiprocessing import Pool
from random import randint, sample
from time import sleep, time
import string

"""What a toddler do when he’s having his nap."""

def nap(toddler):

    time = randint(0,5)
    print(toddler+' Zzz…', end=' ', flush=True)
    sleep(time)
    return(toddler, time)

"""Populate the nursery with toddlers."""

nurserySize   = 24
dormitorySize = 5
toddlers      = []

while (len(toddlers) < nurserySize):

    # Every toddler has a name
    toddlers.append(''.join(sample(string.ascii_lowercase,randint(3,7))).capitalize())

"""For all toddlers, make them nap grouped in available dormitories.
The nap is finished for all the toddlers in a dormitory when the last one wakes up."""

"""1. Only one dormitory, the nap must be finished, then the next group of toddlers can have their own."""

start = time()
barrack = Pool(dormitorySize)
sleepAmount = 0 # Total sleep amount for all toddlers of the nursery

print(str(len(toddlers))+' toddlers going to sleep, '+str(dormitorySize)+' by '+str(dormitorySize)+' in one dormitory.')

for i in range(0, nurserySize, dormitorySize):

    dormitory = barrack.map(nap, toddlers[i:i+dormitorySize])
    #~ print("\n", dormitory)
    for toddler in dormitory:

        sleepAmount += toddler[1]

_time = time() - start
_ratio = sleepAmount/_time

print("\n\n",' → Tooks '+str(_time)+' seconds for '+str(sleepAmount)+' of sleep. ('+str(_ratio)+')',"\n")

"""2. Now the same again, but we have as many dormitories as needed, to nap all the toddlers at once."""

start = time()
barracks = [Pool(dormitorySize)] * (int(nurserySize/dormitorySize) + 1)
sleepAmount = 0
dormitories = []
j = 0

# The callback function will be called when the pool has finished.
def fillDormitory(toddlers):

    dormitories.append(toddlers)

print(str(len(toddlers))+' toddlers going to sleep '+str(dormitorySize)+' by '+str(dormitorySize)+' in '+str(len(barracks))+' dormitories.')

for i in range(0, nurserySize, dormitorySize):

    barracks[j].map_async(nap, toddlers[i:i+dormitorySize], callback=fillDormitory)
    j += 1


for b in barracks:

    b.close()
    b.join()

# Count total sleep amount.
for dormitory in dormitories:

    for toddler in dormitory:

        sleepAmount += toddler[1]

print(dormitories)

_time = time() - start
_ratio = sleepAmount/_time

print("\n",' → Tooks '+str(_time)+' seconds for '+str(sleepAmount)+' of sleep. ('+str(_ratio)+')',"\n")

Je m’attendais avec la deuxième méthode que ça corresponde au fonctionnement suivant : je répartie les nourrissons dans tous les dortoirs disponibles, donc ils commencent tous leur sieste en même temps, c’est la première boucle. Ensuite, deuxième boucle, sur chaque dortoir (avec le close() et le join()) je vais « voir si les nourrissons ont fini leur sieste ». Je pensais qu’ainsi, le temps total correspondrait au temps de sieste du dortoir qui a le plus élevé. Autrement dit, en prenant les dortoirs dans l’ordre dans la seconde boucle, si je dois attendre la fin de la sieste, j’aurais probablement pas à attendre beaucoup pour les suivants, qui auront commencé la sieste en même temps que le premier dortoir.

Mais bien sûr ce n’est pas ça qui se passe…

Voilà ce que j’observe : la deuxième méthode va toujours plus vite que la première, sauf dans le cas où tous les nourrissons dorment un temps identique, là le temps est équivalent à la première méthode !

Pour obtenir ce que je souhaite, c’est à dire que le temps total d’exécution soit égal au temps d’exécution du plus « lent » des pools, j’ai le sentiment que je ne peux pas utiliser map_async() et qu’il faut que je passe par apply_async() et par l’utilisation d’objet Process() individuellement… Mais je sèche un peu… qu’en pensez-vous ?

  • # bug

    Posté par  . Évalué à 2.

    barracks = [Pool(dormitorySize)] * (int(nurserySize/dormitorySize) + 1)

    Probablement pas ce que tu veux faire ("un ensemble de N dortoirs"), tu utilises toujours le même pool.

    In [5]: [ Pool()] * 2
    Out[5]:
    [<multiprocessing.pool.Pool at 0x7f79ba6ff748>,
     <multiprocessing.pool.Pool at 0x7f79ba6ff748>]
    
    • [^] # Re: bug

      Posté par  . Évalué à 2. Dernière modification le 24 décembre 2016 à 18:04.

      Ah oui, bien vu ! Mais du coup je ne comprends pas pourquoi la méthode 2. va plus vite que la méthode 1. (sauf si le temps de sommeil est le même pour tous les nourrissons, là les deux méthodes vont à la même vitesse (?!))

      Évidemment pour faire ce que je veux je pourrais créé un seul pool… mais j’aimerais bien comprendre ce qu’il y a de différent entre 1. et 2.

      Sinon j’ai compris comment utiliser l’objet Queue, qui me permet de lancer plusieurs processus en parallèle et de récupérer leurs résultats au fur et à mesure :

      #!/usr/bin/env python3
      
      from multiprocessing import Process, Queue
      from random import randint, sample
      from time import sleep, time
      import string
      from pprint import pprint
      from ctypes import c_wchar_p
      
      def plop(q, i):
      
          val1 = randint(0,5)
          val2 = ''.join(sample(string.ascii_lowercase,randint(3,7)))
          sleep(val1)
          q.put((i, val1, val2))
      
      items = range(0, 20)
      queue = Queue()
      
      for index in items:
      
          process = Process(target=plop, args=(queue, index))
          process.start()
      
      for index in items:
      
          print(queue.qsize(), "\t", "\t".join(map(str, list(queue.get()))), flush=True)

      Voici la sortie :

      stef@medusa:~/Code/Test$ ./p2.py 
      4    7  0   qfbliw
      3    0  0   hno
      2    13 0   pwd
      1    12 0   zbo
      0    3  1   dxrch
      0    8  1   caig
      0    11 1   prwcf
      0    10 1   ftjbel
      0    15 1   vprcg
      0    1  2   crkpqzo
      1    6  2   qntlr
      0    9  2   lruh
      0    4  3   tdbyip
      0    2  3   inwleyo
      0    14 3   pulrgd
      0    19 3   ikuh
      0    5  4   rkt
      0    16 5   wnzx
      1    17 5   atlnx
      0    18 5   yiejnox
      

      Le première colonne c’est la taille de la file (Queue), deuxième colonne l’index de l’item (qui correspond à l’ordre de lancement du process), en troisième colonne le temps pris par le process (temps de sleep()) et en quatrième colonne la valeur aléatoire générée par chaque process.

      On peut voir que la taille de la file est importante au début… Voilà comment je le comprends : quand on fait le get() dans la deuxième boucle, on commence par le premier item, donc pendant qu’on attend ce premier process les suivants ont possiblement déjà terminé, venant s’enfiler dans queue.

      On a les résultats « dans l’ordre logique », c’est à dire qu’on a les process qui prennent le plus de temps en dernier.

      Avec cette dernière méthode le temps total ne dépasse pas le temps pris par le process qui dure le plus longtemps.

      Par ailleurs j’ai également réussi à utiliser l’objet Manager, qui permet aussi de partager des variables, mais bon… un concept à la fois…

      J’ai l’impression que pour un besoin donné il n’y a pas une seule bonne méthode et qu’en fait on a plutôt le choix, entre utiliser Pool, Queue, Manager, Process ou autre… j’ai encore du mal à bien voir quels sont les avantages et inconvénients de chacun.
      ```

      • [^] # Re: bug

        Posté par  . Évalué à 2.

        J’en profite pour une autre question, qui n’a pas de rapport avec le sujet de départ…

        Dans le script ci-dessus, on peut gagner deux lignes en ne déclarant pas des variables (val2 et process) qui ne sont pas utilisées :

        def plop(q, i):
        
            val = randint(0,5)
            sleep(val)
            q.put((i, val, ''.join(sample(string.ascii_lowercase,randint(3,7)))))
        
        items = range(0, 20)
        queue = Queue()
        
        for index in items:
        
            Process(target=plop, args=(queue, index)).start()
        
        for index in items:
        
            print(queue.qsize(), "\t", "\t".join(map(str, list(queue.get()))), flush=True)

        La question que je me pose est la suivante : « Est-ce que cela change quelque chose au final ? »

        Est-ce que ça change par exemple l’utilisation mémoire ? Dois-je me soucier de ce détail avec Python ou bien puis-je écrire comme cela me semble le plus lisible, l’interpréteur fera le nécessaire ?

  • # Solution

    Posté par  . Évalué à 2. Dernière modification le 24 décembre 2016 à 18:35.

    Encore merci benja !

    Est-ce qu’il est correct de dire qu’en faisant [Pool(N)] * M je fais en fait M références au même objet ?

    Quoi qu’il en soit voici la solution en utilisant plusieurs dortoirs. Maintenant les nourrissons peuvent tous commencer leur sieste en même temps et le temps total est égal au temps de la plus longue sieste.

    Il fallait faire comme ça pour avoir plusieurs pools:

    barracks = []
    
    for i in range(0, int(nurserySize/dormitorySize) + 1):
    
        barracks.append(Pool(dormitorySize))

    Le script en entier (j’ai viré la méthode 1.) :

    #!/usr/bin/env python3
    """Make all the toddlers in the nursery have their nap."""
    
    from multiprocessing import Pool
    from random import randint, sample
    from time import sleep, time
    import string
    from pprint import pprint
    
    """What a toddler does when he’s having his nap."""
    
    def nap(toddler):
    
        time = randint(0,4)
        print(toddler+' Zzz…', end=' ', flush=True)
        sleep(time)
        return(toddler, time)
    
    """Populate the nursery with toddlers."""
    
    nurserySize   = 11
    toddlers      = []
    
    while (len(toddlers) < nurserySize):
    
        # Every toddler has a name
        toddlers.append(''.join(sample(string.ascii_lowercase,randint(3,7))).capitalize())
    
    """For all toddlers, make them nap grouped in available dormitories.
    The nap is finished for all the toddlers in a dormitory when the last one wakes up."""
    
    dormitorySize = 4
    
    """Nap all the toddlers at once."""
    
    start = time()
    barracks = []
    
    for i in range(0, int(nurserySize/dormitorySize) + 1):
    
        barracks.append(Pool(dormitorySize))
    
    sleepAmount = 0
    dormitories = []
    j = 0
    
    # The callback function will be called when the pool has finished.
    def fillDormitory(toddlers):
    
        dormitories.append(toddlers)
    
    print(str(len(toddlers))+' toddlers going to sleep '+str(dormitorySize)+' by '+str(dormitorySize)+' in '+str(len(barracks))+' dormitories.')
    
    for i in range(0, nurserySize, dormitorySize):
    
        barracks[j].map_async(nap, toddlers[i:i+dormitorySize], callback=fillDormitory)
        j += 1
    
    for b in barracks:
    
        b.close()
        b.join()
    
    # Count total sleep amount.
    for dormitory in dormitories:
    
        for toddler in dormitory:
    
            sleepAmount += toddler[1]
    
    print(dormitories)
    
    _time = time() - start
    _ratio = sleepAmount/_time
    
    print("\n",' → Tooks '+str(_time)+' seconds for '+str(sleepAmount)+' of sleep. ('+str(_ratio)+')',"\n")

    EDIT: J’ai le sentiment que je devrais pouvoir me passer d’utiliser un indice explicite (j), ça ne doit pas être très pythonesque comme manière de faire :/

    • [^] # Re: Solution

      Posté par  . Évalué à 2.

      Juste quelques remarques pythonniques :

      • tu peux simplifier
      barracks = []
      
      for i in range(0, int(nurserySize/dormitorySize) + 1):
      
          barracks.append(Pool(dormitorySize))

      en utilisant une compréhension de liste

      barracks = [Pool(dormitorySize) for i in range(0, nurserySize//dormitorySize + 1)]
      • tu peux effectivement remplacer le j par un enumerate :
      for j, i in enumerate(range(0, nurserySize, dormitorySize)):
  • # Vous me corrigerez si je dis des bêtises

    Posté par  . Évalué à 2.

    Mais du coup je ne comprends pas pourquoi la méthode 2. va plus vite que la méthode 1. (sauf si le temps de sommeil est le même pour tous les nourrissons, là les deux méthodes vont à la même vitesse (?!))

    En fait je crois que j’ai compris…

    Là où avec la méthode 1. j’attends que tout le dortoir ait fini, avec la méthode 2. je peux optimiser l’utilisation de mon unique dortoir car je peux faire dormir un nourrisson dès qu’une place se libère, sans devoir attendre que tout le dortoir ait fini…

    C’est bien ça ?

    • [^] # Re: Vous me corrigerez si je dis des bêtises

      Posté par  . Évalué à 2. Dernière modification le 26 décembre 2016 à 01:21.

      Là où avec la méthode 1. j’attends que tout le dortoir ait fini, avec la méthode 2. je peux optimiser l’utilisation de mon unique dortoir car je peux faire dormir un nourrisson dès qu’une place se libère, sans devoir attendre que tout le dortoir ait fini…

      C’est bien ça ?

      je ne sais pas, c'est ton code ou c'est un code que tu as repiqué quelque part ?

      si c'est ton code, tu dois savoir ce que tu voulais faire
      si c'est une reprise, ben faut relire le code, en deduire l'algo, et verifier cette hypothese ;)

      • [^] # Re: Vous me corrigerez si je dis des bêtises

        Posté par  . Évalué à 2.

        C’est mon code. Il ne faisait pas ce que je croyais qu’il faisait, comme me l’a fait remarquer benja ;)

        Enfin c’est bon, j’ai compris maintenant, j’ai pu mettre en application sur le code que je voulais faire à la base (en utilisant Queue et Process directement…). J’en ai fini avec les nourrissons :)

        en deduire l'algo

        L’algo, il commence par « je fais une liste dont tous les éléments sont le même pointeur » (et moi je croyais faire : « je fais une liste de différents pointeurs ») donc je pense qu’il pue…

        L’erreur (assez débile il faut le dire) que j’ai fait c’est de pas penser à ça :

        >>> [randint(0,9)] * 9
        [1, 1, 1, 1, 1, 1, 1, 1, 1]
        
        >>> [randint(0,9)] * 9
        [6, 6, 6, 6, 6, 6, 6, 6, 6]
        

        je me suis attendu à avoir des éléments différents :/ mais ça ne marche pas comme ça, voilà.

Suivre le flux des commentaires

Note : les commentaires appartiennent à celles et ceux qui les ont postés. Nous n’en sommes pas responsables.