Я просмотрел несколько ответов о переполнении стека и в Интернете, пытаясь настроить способ выполнения многопроцессорной обработки с использованием очередей для передачи больших фреймов данных pandas. Мне казалось, что каждый ответ повторял одни и те же решения без учета множества крайних случаев, которые обязательно встретятся при настройке подобных вычислений. Проблема в том, что одновременно задействовано много вещей. Количество задач, количество рабочих, продолжительность каждой задачи и возможные исключения во время выполнения задачи. Все это затрудняет синхронизацию, и в большинстве ответов не рассматривается, как это сделать. Итак, это мое мнение после нескольких часов возни, надеюсь, оно будет достаточно общим, чтобы большинство людей сочло его полезным.
Некоторые мысли перед примерами кодирования. Поскольку queue.Empty
или queue.qsize()
любой другой подобный метод ненадежен для управления потоком, любой подобный код
while True:
try:
task = pending_queue.get_nowait()
except queue.Empty:
break
подделка. Это убьет воркера, даже если через миллисекунды в очереди появится другая задача. Рабочий не восстановится, и через некоторое время ВСЕ рабочие исчезнут, так как они случайно обнаружат, что очередь на мгновение пуста. Конечным результатом будет то, что основная функция многопроцессорности (с функцией join () для процессов) вернется без выполнения всех задач. Ницца. Удачи в отладке, если у вас есть тысячи задач, а некоторые из них отсутствуют.
Другая проблема - это использование контрольных значений. Многие люди предлагали добавить в очередь значение дозорного, чтобы отметить конец очереди. Но кому именно помечать? Если есть N рабочих, предполагая, что N - это количество доступных ядер, то единственное контрольное значение будет указывать только на конец очереди для одного рабочего. Все остальные рабочие будут сидеть и ждать, пока не останется работы. Типичные примеры, которые я видел:
while True:
task = pending_queue.get()
if task == SOME_SENTINEL_VALUE:
break
Один рабочий получит значение дозорного, а остальные будут ждать бесконечно. Ни в одном сообщении, которое я встретил, не упоминалось, что вам нужно отправить значение дозорного в очередь, ПО МЕНЬШЕ, столько раз, сколько у вас есть воркеров, чтобы ВСЕ они его получили.
Другая проблема - обработка исключений во время выполнения задачи. Опять же, их нужно ловить и управлять ими. Более того, если у вас есть completed_tasks
очередь, вы должны независимо детерминированным образом подсчитать, сколько элементов находится в очереди, прежде чем вы решите, что задание выполнено. Опять же, полагаться на размеры очереди обречено на неудачу и возвращать неожиданные результаты.
В приведенном ниже примере par_proc()
функция получит список задач, включая функции, с которыми эти задачи должны выполняться вместе с любыми именованными аргументами и значениями.
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
И вот тест для запуска приведенного выше кода против
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
плюс еще один за некоторыми исключениями
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
Надеюсь, что это поможет.