Eché un vistazo a varias respuestas en el desbordamiento de pila y en la web mientras intentaba configurar una forma de hacer multiprocesamiento utilizando colas para pasar grandes marcos de datos de pandas. Me pareció que cada respuesta estaba reiterando el mismo tipo de soluciones sin tener en cuenta la multitud de casos extremos que uno definitivamente encontrará al configurar cálculos como estos. El problema es que hay muchas cosas en juego al mismo tiempo. El número de tareas, el número de trabajadores, la duración de cada tarea y las posibles excepciones durante la ejecución de la tarea. Todos estos hacen que la sincronización sea complicada y la mayoría de las respuestas no abordan cómo puede hacerlo. Así que esta es mi opinión después de jugar durante unas horas, espero que sea lo suficientemente genérico para que la mayoría de la gente lo encuentre útil.
Algunas reflexiones antes de cualquier ejemplo de codificación. Dado que queue.Empty
o queue.qsize()
cualquier otro método similar no es confiable para el control de flujo, cualquier código similar
while True:
try:
task = pending_queue.get_nowait()
except queue.Empty:
break
es falso. Esto matará al trabajador incluso si milisegundos más tarde aparece otra tarea en la cola. El trabajador no se recuperará y después de un tiempo TODOS los trabajadores desaparecerán ya que al azar encuentran la cola momentáneamente vacía. El resultado final será que la función principal de multiprocesamiento (la que tiene la combinación () en los procesos) regresará sin que se hayan completado todas las tareas. Agradable. Buena suerte depurando eso si tienes miles de tareas y faltan algunas.
El otro problema es el uso de valores centinela. Mucha gente ha sugerido agregar un valor centinela en la cola para marcar el final de la cola. Pero para señalarlo a quién exactamente? Si hay N trabajadores, asumiendo que N es el número de núcleos disponibles más o menos, entonces un solo valor centinela solo marcará el final de la cola a un trabajador. Todos los demás trabajadores se quedarán sentados esperando más trabajo cuando no quede ninguno. Los ejemplos típicos que he visto son
while True:
task = pending_queue.get()
if task == SOME_SENTINEL_VALUE:
break
Un trabajador obtendrá el valor centinela mientras que el resto esperará indefinidamente. Ninguna publicación con la que me encontré mencionó que debe enviar el valor centinela a la cola AL MENOS tantas veces como trabajadores para que TODOS lo obtengan.
El otro problema es el manejo de excepciones durante la ejecución de la tarea. Nuevamente, estos deben ser capturados y manejados. Además, si tiene una completed_tasks
cola, debe contar de forma independiente y determinista cuántos elementos hay en la cola antes de decidir que el trabajo está terminado. De nuevo, depender del tamaño de las colas está destinado a fallar y arrojar resultados inesperados.
En el siguiente ejemplo, la par_proc()
función recibirá una lista de tareas que incluye las funciones con las que estas tareas deben ejecutarse junto con los argumentos y valores nombrados.
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
Y aquí hay una prueba para ejecutar el código anterior 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
más otro con algunas excepciones
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 sea de ayuda.