Saya telah melihat beberapa jawaban di seluruh stack overflow dan web saat mencoba menyiapkan cara melakukan multiprosesing menggunakan antrean untuk menyebarkan kerangka data panda besar. Tampak bagi saya bahwa setiap jawaban mengulangi jenis solusi yang sama tanpa mempertimbangkan banyaknya kasus tepi yang pasti akan ditemukan seseorang saat menyiapkan penghitungan seperti ini. Masalahnya adalah ada banyak hal yang berperan pada saat yang bersamaan. Jumlah tugas, jumlah pekerja, durasi setiap tugas dan kemungkinan pengecualian selama pelaksanaan tugas. Semua ini membuat sinkronisasi menjadi rumit dan sebagian besar jawaban tidak membahas bagaimana Anda dapat melakukannya. Jadi ini adalah pendapat saya setelah bermain-main selama beberapa jam, semoga ini cukup umum bagi kebanyakan orang untuk menganggapnya berguna.
Beberapa pemikiran sebelum contoh pengkodean. Karena queue.Empty
atau queue.qsize()
metode serupa lainnya tidak dapat diandalkan untuk kontrol aliran, kode sejenisnya
while True:
try:
task = pending_queue.get_nowait()
except queue.Empty:
break
itu palsu. Ini akan mematikan pekerja bahkan jika milidetik kemudian tugas lain muncul dalam antrian. Pekerja tidak akan pulih dan setelah beberapa saat SEMUA pekerja akan menghilang karena mereka secara acak menemukan antrian kosong untuk sementara. Hasil akhirnya adalah fungsi multiprosesing utama (yang memiliki join () pada proses) akan kembali tanpa semua tugas diselesaikan. Bagus. Semoga berhasil melakukan debug melalui itu jika Anda memiliki ribuan tugas dan beberapa hilang.
Masalah lainnya adalah penggunaan nilai sentinel. Banyak orang menyarankan untuk menambahkan nilai sentinel dalam antrian untuk menandai akhir antrian. Tetapi untuk menandainya kepada siapa sebenarnya? Jika ada N pekerja, dengan asumsi N adalah jumlah inti yang tersedia memberi atau menerima, maka satu nilai sentinel hanya akan menandai akhir antrian untuk satu pekerja. Semua pekerja lainnya akan duduk menunggu lebih banyak pekerjaan ketika tidak ada yang tersisa. Contoh khas yang pernah saya lihat adalah
while True:
task = pending_queue.get()
if task == SOME_SENTINEL_VALUE:
break
Satu pekerja akan mendapatkan nilai sentinel sedangkan pekerja lainnya akan menunggu tanpa batas. Tidak ada pos yang saya temukan menyebutkan bahwa Anda perlu mengirimkan nilai sentinel ke antrean SETIDAKNYA sebanyak Anda memiliki pekerja sehingga SEMUA mereka mendapatkannya.
Masalah lainnya adalah penanganan pengecualian selama eksekusi tugas. Sekali lagi ini harus ditangkap dan dikelola. Selain itu, jika Anda memiliki completed_tasks
antrian, Anda harus menghitung secara deterministik berapa banyak item dalam antrian sebelum Anda memutuskan bahwa pekerjaan telah selesai. Sekali lagi mengandalkan ukuran antrian pasti akan gagal dan mengembalikan hasil yang tidak diharapkan.
Dalam contoh di bawah ini, par_proc()
fungsi akan menerima daftar tugas termasuk fungsi yang harus dieksekusi bersama dengan argumen dan nilai bernama apa pun.
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
Dan berikut ini adalah tes untuk menjalankan kode di atas
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
ditambah satu sama lain dengan beberapa pengecualian
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
Semoga bermanfaat.