Eu dei uma olhada em várias respostas em overflow de pilha e na web enquanto tentava configurar uma maneira de fazer multiprocessamento usando filas para passar grandes dataframes de pandas. Pareceu-me que cada resposta estava reiterando o mesmo tipo de solução, sem qualquer consideração da multiplicidade de casos extremos que definitivamente encontraremos ao configurar cálculos como esses. O problema é que há muitas coisas em jogo ao mesmo tempo. O número de tarefas, o número de trabalhadores, a duração de cada tarefa e possíveis exceções durante a execução da tarefa. Tudo isso torna a sincronização complicada e a maioria das respostas não aborda como você pode fazer isso. Portanto, esta é a minha opinião depois de mexer por algumas horas, espero que seja genérico o suficiente para a maioria das pessoas considerá-lo útil.
Algumas reflexões antes de qualquer exemplo de codificação. Como queue.Empty
ou queue.qsize()
qualquer outro método semelhante não é confiável para o controle de fluxo, qualquer código do mesmo
while True:
try:
task = pending_queue.get_nowait()
except queue.Empty:
break
é falso. Isso matará o trabalhador, mesmo se, milissegundos depois, outra tarefa aparecer na fila. O trabalhador não irá se recuperar e depois de um tempo TODOS os trabalhadores desaparecerão, pois eles encontram a fila momentaneamente vazia. O resultado final será que a função de multiprocessamento principal (aquela com join () nos processos) retornará sem que todas as tarefas tenham sido concluídas. Agradável. Boa sorte na depuração se você tiver milhares de tarefas e algumas estiverem faltando.
A outra questão é o uso de valores sentinela. Muitas pessoas sugeriram adicionar um valor de sentinela na fila para sinalizar o fim da fila. Mas sinalizar para quem exatamente? Se houver N trabalhadores, assumindo que N seja o número de núcleos disponíveis mais ou menos, um único valor sentinela sinalizará apenas o fim da fila para um trabalhador. Todos os outros trabalhadores ficarão sentados esperando por mais trabalho quando não houver mais nenhum. Exemplos típicos que vi são
while True:
task = pending_queue.get()
if task == SOME_SENTINEL_VALUE:
break
Um trabalhador obterá o valor da sentinela enquanto o restante aguardará indefinidamente. Nenhuma postagem que encontrei mencionou que você precisa enviar o valor sentinela para a fila PELO MENOS quantas vezes você tiver trabalhadores, para que TODOS eles o recebam.
O outro problema é o tratamento de exceções durante a execução da tarefa. Novamente, eles devem ser capturados e gerenciados. Além disso, se você tiver uma completed_tasks
fila, deverá contar independentemente de forma determinística quantos itens estão na fila antes de decidir que o trabalho está concluído. Mais uma vez, confiar nos tamanhos das filas está fadado ao fracasso e retorna resultados inesperados.
No exemplo abaixo, a par_proc()
função receberá uma lista de tarefas incluindo as funções com as quais essas tarefas devem ser executadas junto com quaisquer argumentos e valores nomeados.
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
E aqui está um teste para executar o código acima contra
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
mais outro com algumas exceções
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
Espero que isso seja útil.