ฉันได้ดูคำตอบหลายคำตอบในสแต็กโอเวอร์โฟลว์และเว็บในขณะที่พยายามตั้งค่าวิธีการประมวลผลหลายขั้นตอนโดยใช้คิวเพื่อส่งผ่านดาต้าเฟรมของแพนด้าขนาดใหญ่ สำหรับฉันแล้วดูเหมือนว่าทุกคำตอบกำลังทำซ้ำวิธีการแก้ปัญหาแบบเดิมโดยไม่ต้องคำนึงถึงกรณีขอบจำนวนมากที่เราจะเจอเมื่อตั้งค่าการคำนวณเช่นนี้ ปัญหาคือมีหลายอย่างในเวลาเดียวกัน จำนวนงานจำนวนคนงานระยะเวลาของแต่ละงานและข้อยกเว้นที่เป็นไปได้ระหว่างการดำเนินงาน สิ่งเหล่านี้ทำให้การซิงโครไนซ์ยุ่งยากและคำตอบส่วนใหญ่ไม่ได้อยู่ที่ว่าคุณจะทำอย่างไร นี่คือสิ่งที่ฉันใช้หลังจากเล่นซอไปสองสามชั่วโมงหวังว่านี่จะเป็นเรื่องทั่วไปเพียงพอสำหรับคนส่วนใหญ่ที่จะพบว่ามีประโยชน์
ความคิดบางอย่างก่อนตัวอย่างการเข้ารหัสใด ๆ เนื่องจากqueue.Empty
หรือqueue.qsize()
หรือวิธีการอื่นที่คล้ายคลึงกันไม่น่าเชื่อถือสำหรับการควบคุมการไหลรหัสใด ๆ ที่คล้ายกัน
while True:
try:
task = pending_queue.get_nowait()
except queue.Empty:
break
เป็นของปลอม สิ่งนี้จะฆ่าคนงานแม้ว่ามิลลิวินาทีต่อมามีงานอื่นปรากฏขึ้นในคิว คนงานจะไม่ฟื้นตัวและหลังจากนั้นไม่นานคนงานทั้งหมดจะหายไปเมื่อพวกเขาสุ่มพบว่าคิวว่างเปล่าชั่วขณะ ผลลัพธ์สุดท้ายคือฟังก์ชันมัลติโพรเซสซิงหลัก (ฟังก์ชันที่มีการเข้าร่วม () บนโปรเซส) จะส่งคืนโดยที่งานทั้งหมดไม่เสร็จสมบูรณ์ ดี. ขอให้โชคดีในการแก้ไขข้อบกพร่องนั้นหากคุณมีงานหลายพันงานและขาดหายไป
ปัญหาอื่น ๆ คือการใช้ค่า Sentinel หลายคนแนะนำให้เพิ่มค่า sentinel ในคิวเพื่อตั้งค่าสถานะจุดสิ้นสุดของคิว แต่จะฟันธงว่าใครกันแน่? หากมีคนงาน N สมมติว่า N คือจำนวนคอร์ที่มีให้หรือรับค่า Sentinel เดียวจะตั้งค่าสถานะจุดสิ้นสุดของคิวให้กับผู้ปฏิบัติงานหนึ่งคนเท่านั้น คนงานคนอื่น ๆ ทั้งหมดจะนั่งรองานเพิ่มเมื่อไม่มีเหลือ ตัวอย่างทั่วไปที่ฉันเคยเห็นคือ
while True:
task = pending_queue.get()
if task == SOME_SENTINEL_VALUE:
break
คนงานคนหนึ่งจะได้รับค่า Sentinel ในขณะที่คนที่เหลือจะรอไปเรื่อย ๆ ไม่มีโพสต์ที่ฉันพูดถึงว่าคุณต้องส่งค่า Sentinel ไปยังคิวอย่างน้อยหลาย ๆ ครั้งตามที่คุณมีคนงานเพื่อให้ทุกคนได้รับ
ปัญหาอื่น ๆ คือการจัดการข้อยกเว้นระหว่างการดำเนินงาน อีกครั้งสิ่งเหล่านี้ควรถูกจับและจัดการ ยิ่งไปกว่านั้นหากคุณมีไฟล์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
หวังว่าจะเป็นประโยชน์