web-dev-qa-db-fra.com

Comment générer des processus enfants parallèles sur un système multiprocesseur?

J'ai un script Python que je veux utiliser comme contrôleur pour un autre script Python. J'ai un serveur avec 64 processeurs, donc je veux générer jusqu'à 64 processus enfants de ce second script Python. Le script enfant est appelé:

$ python create_graphs.py --name=NAME

où NAME est quelque chose comme XYZ, ABC, NYU etc.

Dans mon script de contrôleur parent, je récupère la variable de nom dans une liste:

my_list = [ 'XYZ', 'ABC', 'NYU' ]

Ma question est donc la suivante: quelle est la meilleure façon d'engendrer ces processus en tant qu'enfants? Je veux limiter le nombre d'enfants à 64 à la fois, donc je dois suivre l'état (si le processus enfant est terminé ou non) afin que je puisse efficacement garder toute la génération en marche.

J'ai cherché à utiliser le package de sous-processus, mais je l'ai rejeté car il ne génère qu'un enfant à la fois. J'ai finalement trouvé le package multiprocesseur, mais j'avoue être submergé par l'ensemble de la documentation sur les threads et les sous-processus.

En ce moment, mon script utilise subprocess.call pour engendrer un seul enfant à la fois et ressemble à ceci:

#!/path/to/python
import subprocess, multiprocessing, Queue
from multiprocessing import Process

my_list = [ 'XYZ', 'ABC', 'NYU' ]

if __== '__main__':
    processors = multiprocessing.cpu_count()

    for i in range(len(my_list)):
        if( i < processors ):
             cmd = ["python", "/path/to/create_graphs.py", "--name="+ my_list[i]]
             child = subprocess.call( cmd, Shell=False )

Je veux vraiment qu'il fasse apparaître 64 enfants à la fois. Dans d'autres questions de stackoverflow, j'ai vu des gens utiliser Queue, mais il semble que cela crée un impact sur les performances?

42
tatlar

Ce que vous recherchez est la classe pool de processus en multitraitement.

import multiprocessing
import subprocess

def work(cmd):
    return subprocess.call(cmd, Shell=False)

if __== '__main__':
    count = multiprocessing.cpu_count()
    pool = multiprocessing.Pool(processes=count)
    print pool.map(work, ['ls'] * count)

Et voici un exemple de calcul pour le rendre plus facile à comprendre. Ce qui suit divisera 10000 tâches sur N processus où N est le nombre de processeurs. Notez que je passe None comme nombre de processus. Cela entraînera la classe Pool à utiliser cpu_count pour le nombre de processus ( référence )

import multiprocessing
import subprocess

def calculate(value):
    return value * 10

if __== '__main__':
    pool = multiprocessing.Pool(None)
    tasks = range(10000)
    results = []
    r = pool.map_async(calculate, tasks, callback=results.append)
    r.wait() # Wait on the results
    print results
60
Nadia Alramli

Voici la solution que j'ai trouvée, basée sur les commentaires de Nadia et Jim. Je ne sais pas si c'est la meilleure façon, mais ça marche. Le script enfant d'origine appelé doit être un script Shell, car j'ai besoin d'utiliser des applications tierces, y compris Matlab. J'ai donc dû le retirer de Python et le coder en bash.

import sys
import os
import multiprocessing
import subprocess

def work(staname):
    print 'Processing station:',staname
    print 'Parent process:', os.getppid()
    print 'Process id:', os.getpid()
    cmd = [ "/bin/bash" "/path/to/executable/create_graphs.sh","--name=%s" % (staname) ]
    return subprocess.call(cmd, Shell=False)

if __== '__main__':

    my_list = [ 'XYZ', 'ABC', 'NYU' ]

    my_list.sort()

    print my_list

    # Get the number of processors available
    num_processes = multiprocessing.cpu_count()

    threads = []

    len_stas = len(my_list)

    print "+++ Number of stations to process: %s" % (len_stas)

    # run until all the threads are done, and there is no data left

    for list_item in my_list:

        # if we aren't using all the processors AND there is still data left to
        # compute, then spawn another thread

        if( len(threads) < num_processes ):

            p = multiprocessing.Process(target=work,args=[list_item])

            p.start()

            print p, p.is_alive()

            threads.append(p)

        else:

            for thread in threads:

                if not thread.is_alive():

                    threads.remove(thread)

Cela semble-t-il une solution raisonnable? J'ai essayé d'utiliser le format de boucle while de Jim, mais mon script n'a rien retourné. Je ne sais pas trop pourquoi. Voici la sortie lorsque j'exécute le script avec la boucle "while" de Jim remplaçant la boucle "for":

hostname{me}2% controller.py 
['ABC', 'NYU', 'XYZ']
Number of processes: 64
+++ Number of stations to process: 3
hostname{me}3%

Lorsque je l'exécute avec la boucle 'for', j'obtiens quelque chose de plus significatif:

hostname{me}6% controller.py 
['ABC', 'NYU', 'XYZ']
Number of processes: 64
+++ Number of stations to process: 3
Processing station: ABC
Parent process: 1056
Process id: 1068
Processing station: NYU
Parent process: 1056
Process id: 1069
Processing station: XYZ
Parent process: 1056
Process id: 1071
hostname{me}7%

Donc ça marche, et je suis content. Cependant, je ne comprends toujours pas pourquoi je ne peux pas utiliser la boucle de style "while" de Jim au lieu de la boucle "for" que j'utilise. Merci pour toute l'aide - je suis impressionné par l'étendue des connaissances @ stackoverflow.

2
tatlar

J'utiliserais certainement multiprocessing plutôt que de rouler ma propre solution en utilisant un sous-processus.

1
Aaron Maenpaa

Je ne pense pas que vous ayez besoin d'une file d'attente à moins que vous n'ayez l'intention d'extraire des données des applications (ce qui si vous voulez des données, je pense qu'il serait plus facile de les ajouter à une base de données de toute façon)

mais essayez ceci pour la taille:

placez le contenu de votre script create_graphs.py dans une fonction appelée "create_graphs"

import threading
from create_graphs import create_graphs

num_processes = 64
my_list = [ 'XYZ', 'ABC', 'NYU' ]

threads = []

# run until all the threads are done, and there is no data left
while threads or my_list:

    # if we aren't using all the processors AND there is still data left to
    # compute, then spawn another thread
    if (len(threads) < num_processes) and my_list:
        t = threading.Thread(target=create_graphs, args=[ my_list.pop() ])
        t.setDaemon(True)
        t.start()
        threads.append(t)

    # in the case that we have the maximum number of threads check if any of them
    # are done. (also do this when we run out of data, until all the threads are done)
    else:
        for thread in threads:
            if not thread.isAlive():
                threads.remove(thread)

Je sais que cela entraînera 1 threads de moins que les processeurs, ce qui est probablement bon, il laisse un processeur pour gérer les threads, les E/S de disque et d'autres choses qui se passent sur l'ordinateur. Si vous décidez d'utiliser le dernier noyau, ajoutez-en un

edit : Je pense que j'ai peut-être mal interprété le but de my_list. Tu n'as pas besoin my_list pour garder une trace des threads (car ils sont tous référencés par les éléments de la liste threads). Mais c'est une bonne façon d'alimenter l'entrée des processus - ou mieux encore: utiliser une fonction générateur;)

Le but de my_list et threads

my_list contient les données que vous devez traiter dans votre fonction
threads n'est qu'une liste des threads en cours d'exécution

la boucle while fait deux choses, démarre de nouveaux threads pour traiter les données et vérifie si des threads ont été exécutés.

Donc, tant que vous avez soit (a) plus de données à traiter, soit (b) des threads dont l'exécution n'est pas terminée ... vous voulez programmer pour continuer à fonctionner. Une fois les deux listes vides, elles seront évaluées à False et la boucle while se fermera

1
Jiaaro