Я переглянув декілька відповідей у переповненнях стеків та в Інтернеті, намагаючись налаштувати спосіб багатопроцесорної обробки, використовуючи черги для обходу великих кадрів даних панд. Мені здавалося, що кожна відповідь повторює однотипні рішення без будь-якого врахування безлічі крайових випадків, які неодмінно траплятимуться при налаштуванні подібних розрахунків. Проблема в тому, що одночасно грає багато речей. Кількість завдань, кількість робітників, тривалість кожного завдання та можливі винятки під час виконання завдання. Все це робить синхронізацію складним, і більшість відповідей не стосуються того, як ви можете це зробити. Отже, це моя думка після того, як повозитись кілька годин, сподіваюся, це буде достатньо загальним для більшості людей, щоб визнати це корисним.
Деякі думки перед будь-якими прикладами кодування. Оскільки queue.Empty
або queue.qsize()
будь-який інший подібний метод ненадійний для управління потоком, будь-який подібний код
while True:
try:
task = pending_queue.get_nowait()
except queue.Empty:
break
є фіктивним. Це призведе до вбивства працівника, навіть якщо через мілісекунди в чергу з’явиться інше завдання. Працівник не відновиться, і через деякий час ВСІ працівники зникнуть, оскільки вони випадково виявлять чергу на мить порожньою. Кінцевим результатом буде те, що основна функція багатопроцесорної обробки (та, що має з'єднання () на процесах) повернеться без завершення всіх завдань. Приємно. Успіху, налагоджуючи це, якщо у вас є тисячі завдань, а деякі відсутні.
Інше питання - використання вартових значень. Багато людей пропонують додати вартове значення в чергу, щоб позначити кінець черги. Але кому це позначити? Якщо є 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
Сподіваюся, що це корисно.