大きなパンダのデータフレームを渡すためにキューを使用してマルチプロセッシングを実行する方法を設定しようとしているときに、スタックオーバーフローとWeb全体にわたる複数の回答を調べました。このような計算を設定するときに必ず遭遇する多数のエッジケースを考慮せずに、すべての回答が同じ種類のソリューションを繰り返しているように見えました。問題は、同時に多くのことが行われていることです。タスクの数、ワーカーの数、各タスクの期間、およびタスク実行中に発生する可能性のある例外。これらはすべて同期をトリッキーにし、ほとんどの回答は同期をどのように行うことができるかを扱っていません。ですから、これは数時間いじった後の私の見解です。うまくいけば、これはほとんどの人がそれが役立つと思うのに十分一般的であるでしょう。
コーディング例の前のいくつかの考え。以降queue.Empty
またはqueue.qsize()
または任意の他の同様の方法は、のような任意のコードフロー制御のために信頼できません
while True:
try:
task = pending_queue.get_nowait()
except queue.Empty:
break
偽物です。これにより、数ミリ秒後に別のタスクがキューに表示された場合でも、ワーカーが強制終了されます。ワーカーは回復せず、しばらくすると、キューが一時的に空になっていることがランダムに見つかるため、すべてのワーカーが消えます。最終的な結果として、メインのマルチプロセッシング関数(プロセスにjoin()がある関数)は、すべてのタスクが完了せずに返されます。いいね。何千ものタスクがあり、いくつかが欠落している場合は、それを介してデバッグを頑張ってください。
もう1つの問題は、番兵値の使用です。多くの人が、キューの終わりにフラグを立てるために、キューに番兵の値を追加することを提案しています。しかし、正確に誰にフラグを立てるには?N個のワーカーが存在する場合、Nがギブまたはテイクで使用可能なコアの数であるとすると、単一の番兵値は1人のワーカーに対してキューの終わりにフラグを立てるだけです。他のすべての労働者は、何も残っていないときに、さらなる仕事を待って座っています。私が見た典型的な例は
while True:
task = pending_queue.get()
if task == SOME_SENTINEL_VALUE:
break
1人の労働者は番兵の値を取得し、残りの労働者は無期限に待機します。私が出くわした投稿では、すべてのワーカーがそれを取得できるように、少なくともワーカーの数だけ番兵の値をキューに送信する必要があると述べていません。
もう1つの問題は、タスク実行中の例外の処理です。繰り返しますが、これらはキャッチして管理する必要があります。さらに、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
お役に立てば幸いです。