J'ai jeté un coup d'œil à plusieurs réponses sur le débordement de pile et sur le Web tout en essayant de mettre en place un moyen de faire du multitraitement en utilisant des files d'attente pour faire circuler de grands cadres de données pandas. Il me semblait que chaque réponse était de réitérer le même type de solutions sans aucune considération de la multitude de cas extrêmes que l'on rencontrera certainement lors de la mise en place de calculs comme ceux-ci. Le problème est qu'il y a plusieurs choses en jeu en même temps. Le nombre de tâches, le nombre de travailleurs, la durée de chaque tâche et les exceptions possibles lors de l'exécution de la tâche. Tout cela rend la synchronisation délicate et la plupart des réponses ne traitent pas de la façon dont vous pouvez vous y prendre. C'est donc mon avis après avoir bidouillé pendant quelques heures, j'espère que ce sera assez générique pour que la plupart des gens le trouvent utile.
Quelques réflexions avant tout exemple de codage. Étant donné que queue.Empty
ou queue.qsize()
ou toute autre méthode similaire n'est pas fiable pour le contrôle de flux, tout code similaire
while True:
try:
task = pending_queue.get_nowait()
except queue.Empty:
break
est faux. Cela tuera le worker même si quelques millisecondes plus tard, une autre tâche apparaît dans la file d'attente. Le travailleur ne récupérera pas et après un certain temps TOUS les travailleurs disparaîtront car ils trouveront au hasard la file d'attente momentanément vide. Le résultat final sera que la fonction principale de multitraitement (celle avec la jointure () sur les processus) reviendra sans que toutes les tâches soient terminées. Agréable. Bonne chance pour le débogage si vous avez des milliers de tâches et que quelques-unes manquent.
L'autre problème est l'utilisation des valeurs sentinelles. De nombreuses personnes ont suggéré d'ajouter une valeur sentinelle dans la file d'attente pour marquer la fin de la file d'attente. Mais le signaler à qui exactement? S'il y a N nœuds de calcul, en supposant que N est le nombre de cœurs disponibles à donner ou à prendre, une seule valeur sentinelle marquera uniquement la fin de la file d'attente pour un seul opérateur. Tous les autres travailleurs resteront assis en attendant plus de travail quand il n'en restera plus. Les exemples typiques que j'ai vus sont
while True:
task = pending_queue.get()
if task == SOME_SENTINEL_VALUE:
break
Un ouvrier recevra la valeur sentinelle tandis que le reste attendra indéfiniment. Aucun message que j'ai rencontré n'a mentionné que vous deviez soumettre la valeur sentinelle à la file d'attente AU MOINS autant de fois que vous avez des ouvriers pour que TOUS l'obtiennent.
L'autre problème est la gestion des exceptions lors de l'exécution de la tâche. Encore une fois, ceux-ci doivent être capturés et gérés. De plus, si vous avez une completed_tasks
file d'attente, vous devez compter indépendamment de manière déterministe le nombre d'éléments dans la file d'attente avant de décider que le travail est terminé. Encore une fois, le fait de s'appuyer sur la taille des files d'attente est voué à l'échec et renvoie des résultats inattendus.
Dans l'exemple ci-dessous, la par_proc()
fonction recevra une liste de tâches comprenant les fonctions avec lesquelles ces tâches doivent être exécutées à côté des arguments et valeurs nommés.
import multiprocessing as mp
import dill as pickle
import queue
import time
import psutil
SENTINEL = None
def do_work(tasks_pending, tasks_completed):
worker_name = mp.current_process().name
while True:
try:
task = tasks_pending.get_nowait()
except queue.Empty:
print(worker_name + ' found an empty queue. Sleeping for a while before checking again...')
time.sleep(0.01)
else:
try:
if task == SENTINEL:
print(worker_name + ' no more work left to be done. Exiting...')
break
print(worker_name + ' received some work... ')
time_start = time.perf_counter()
work_func = pickle.loads(task['func'])
result = work_func(**task['task'])
tasks_completed.put({work_func.__name__: result})
time_end = time.perf_counter() - time_start
print(worker_name + ' done in {} seconds'.format(round(time_end, 5)))
except Exception as e:
print(worker_name + ' task failed. ' + str(e))
tasks_completed.put({work_func.__name__: None})
def par_proc(job_list, num_cpus=None):
if not num_cpus:
num_cpus = psutil.cpu_count(logical=False)
print('* Parallel processing')
print('* Running on {} cores'.format(num_cpus))
tasks_pending = mp.Queue()
tasks_completed = mp.Queue()
processes = []
results = []
num_tasks = 0
for job in job_list:
for task in job['tasks']:
expanded_job = {}
num_tasks = num_tasks + 1
expanded_job.update({'func': pickle.dumps(job['func'])})
expanded_job.update({'task': task})
tasks_pending.put(expanded_job)
num_workers = num_cpus
for c in range(num_workers):
tasks_pending.put(SENTINEL)
print('* Number of tasks: {}'.format(num_tasks))
for c in range(num_workers):
p = mp.Process(target=do_work, args=(tasks_pending, tasks_completed))
p.name = 'worker' + str(c)
processes.append(p)
p.start()
completed_tasks_counter = 0
while completed_tasks_counter < num_tasks:
results.append(tasks_completed.get())
completed_tasks_counter = completed_tasks_counter + 1
for p in processes:
p.join()
return results
Et voici un test pour exécuter le code ci-dessus contre
def test_parallel_processing():
def heavy_duty1(arg1, arg2, arg3):
return arg1 + arg2 + arg3
def heavy_duty2(arg1, arg2, arg3):
return arg1 * arg2 * arg3
task_list = [
{'func': heavy_duty1, 'tasks': [{'arg1': 1, 'arg2': 2, 'arg3': 3}, {'arg1': 1, 'arg2': 3, 'arg3': 5}]},
{'func': heavy_duty2, 'tasks': [{'arg1': 1, 'arg2': 2, 'arg3': 3}, {'arg1': 1, 'arg2': 3, 'arg3': 5}]},
]
results = par_proc(task_list)
job1 = sum([y for x in results if 'heavy_duty1' in x.keys() for y in list(x.values())])
job2 = sum([y for x in results if 'heavy_duty2' in x.keys() for y in list(x.values())])
assert job1 == 15
assert job2 == 21
plus un autre avec quelques exceptions
def test_parallel_processing_exceptions():
def heavy_duty1_raises(arg1, arg2, arg3):
raise ValueError('Exception raised')
return arg1 + arg2 + arg3
def heavy_duty2(arg1, arg2, arg3):
return arg1 * arg2 * arg3
task_list = [
{'func': heavy_duty1_raises, 'tasks': [{'arg1': 1, 'arg2': 2, 'arg3': 3}, {'arg1': 1, 'arg2': 3, 'arg3': 5}]},
{'func': heavy_duty2, 'tasks': [{'arg1': 1, 'arg2': 2, 'arg3': 3}, {'arg1': 1, 'arg2': 3, 'arg3': 5}]},
]
results = par_proc(task_list)
job1 = sum([y for x in results if 'heavy_duty1' in x.keys() for y in list(x.values())])
job2 = sum([y for x in results if 'heavy_duty2' in x.keys() for y in list(x.values())])
assert not job1
assert job2 == 21
J'espère que c'est utile.