Ho dato un'occhiata a più risposte attraverso lo stack overflow e il Web mentre cercavo di impostare un modo per eseguire il multiprocessing utilizzando le code per il passaggio di frame di dati di grandi dimensioni. Mi sembrava che ogni risposta ripetesse lo stesso tipo di soluzioni senza alcuna considerazione della moltitudine di casi limite che si incontreranno sicuramente quando si impostano calcoli come questi. Il problema è che ci sono molte cose in gioco allo stesso tempo. Il numero di attività, il numero di lavoratori, la durata di ciascuna attività e le possibili eccezioni durante l'esecuzione dell'attività. Tutto ciò rende la sincronizzazione complicata e la maggior parte delle risposte non indica come puoi procedere. Quindi questa è la mia opinione dopo aver giocherellato per alcune ore, spero che sia abbastanza generico da consentire alla maggior parte delle persone di trovarlo utile.
Alcuni pensieri prima di qualsiasi esempio di codifica. Poiché queue.Empty
o queue.qsize()
o qualsiasi altro metodo simile è inaffidabile per il controllo del flusso, qualsiasi codice simile
while True:
try:
task = pending_queue.get_nowait()
except queue.Empty:
break
è fasullo. Questo ucciderà il lavoratore anche se millisecondi dopo un'altra attività si presenterà nella coda. Il lavoratore non si riprenderà e dopo un po 'TUTTI i lavoratori scompariranno poiché troveranno casualmente la coda momentaneamente vuota. Il risultato finale sarà che la funzione multiprocessing principale (quella con join () sui processi) tornerà senza che tutte le attività siano state completate. Bello. Buona fortuna per eseguire il debug se hai migliaia di attività e alcune ne mancano.
L'altro problema è l'uso dei valori sentinella. Molte persone hanno suggerito di aggiungere un valore sentinella nella coda per contrassegnare la fine della coda. Ma per segnalarlo esattamente a chi? Se sono presenti N lavoratori, supponendo che N sia il numero di core disponibili dare o ricevere, un singolo valore sentinella segnalerà solo la fine della coda a un lavoratore. Tutti gli altri lavoratori si siederanno in attesa di altro lavoro quando non ne rimane nessuno. Esempi tipici che ho visto sono
while True:
task = pending_queue.get()
if task == SOME_SENTINEL_VALUE:
break
Un lavoratore riceverà il valore sentinella mentre il resto aspetterà indefinitamente. Nessun post in cui mi sono imbattuto ha detto che è necessario inviare il valore sentinella alla coda ALMENO tante volte quante sono i lavoratori in modo che TUTTI lo ricevano.
L'altro problema è la gestione delle eccezioni durante l'esecuzione dell'attività. Anche in questo caso questi dovrebbero essere catturati e gestiti. Inoltre, se hai un filecompleted_tasks
coda dovresti contare indipendentemente in modo deterministico quanti elementi ci sono nella coda prima di decidere che il lavoro è finito. Anche in questo caso fare affidamento sulle dimensioni della coda è destinato a fallire e restituisce risultati imprevisti.
Nell'esempio seguente, la par_proc()
funzione riceverà un elenco di attività comprese le funzioni con cui queste attività dovrebbero essere eseguite insieme a qualsiasi argomento e valore con nome.
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
Ed ecco un test per eseguire il codice sopra
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
più un altro con alcune eccezioni
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
Spero che sia utile.