[英]Python JoinableQueue call task_done in other process need twice
我已經實現了一個基於multiprocessing.Process
和JoinableQueue
的 WorkerManager。 雖然我嘗試在 proc.join(timeout) 之后處理超時或取消處理異常等進程異常,並評估 proc.exitcode 以確定如何處理,然后調用 in_queue.task_done() 以通知作業已完成異常處理邏輯。 但是它需要調用兩次。 我不知道為什么它應該被調用兩次。 有沒有人可以在這里找出原因。
整個代碼片段:
# -*- coding=utf-8 -*-
import time
import threading
from queue import Empty
from multiprocessing import Event, Process, JoinableQueue, cpu_count, current_process
TIMEOUT = 3
class WorkersManager(object):
def __init__(self, jobs, processes_num):
self._processes_num = processes_num if processes_num else cpu_count()
self._workers_num = processes_num
self._in_queue, self._run_queue, self._out_queue = JoinableQueue(), JoinableQueue(), JoinableQueue()
self._spawned_procs = []
self._total = 0
self._stop_event = Event()
self._jobs_on_procs = {}
self._wk_kwargs = dict(
in_queue=self._in_queue, run_queue=self._run_queue, out_queue=self._out_queue,
stop_event=self._stop_event
)
self._in_stream = [j for j in jobs]
self._out_stream = []
self._total = len(self._in_stream)
def run(self):
# Spawn Worker
worker_processes = [
WorkerProcess(i, **self._wk_kwargs) for i in range(self._processes_num)
]
self._spawned_procs = [
Process(target=process.run, args=tuple())
for process in worker_processes
]
for p in self._spawned_procs:
p.start()
self._serve()
monitor = threading.Thread(target=self._monitor, args=tuple())
monitor.start()
collector = threading.Thread(target=self._collect, args=tuple())
collector.start()
self._join_workers()
# TODO: Terminiate threads
monitor.join(TIMEOUT)
collector.join(TIMEOUT)
self._in_queue.join()
self._out_queue.join()
return self._out_stream
def _join_workers(self):
for p in self._spawned_procs:
p.join(TIMEOUT)
if p.is_alive():
p.terminate()
job = self._jobs_on_procs.get(p.name)
print('Process TIMEOUT: {0} {1}'.format(p.name, job))
result = {
"status": "failed"
}
self._out_queue.put(result)
for _ in range(2):
# NOTE: Call task_done twice
# Guessing:
# 1st time to swtich process?
# 2nd time to notify task has done?
# TODO: figure it out why?
self._in_queue.task_done()
else:
if p.exitcode == 0:
print("{} exit with code:{}".format(p, p.exitcode))
else:
job = self._jobs_on_procs.get(p.name)
if p.exitcode > 0:
print("{} with code:{} {}".format(p, p.exitcode, job))
else:
print("{} been killed with code:{} {}".format(p, p.exitcode, job))
result = {
"status": "failed"
}
self._out_queue.put(result)
for _ in range(2):
# NOTE: Call task_done twice
# Guessing:
# 1st time to swtich process?
# 2nd time to notify task has done?
# TODO: figure it out why?
self._in_queue.task_done()
def _collect(self):
# TODO: Spawn a collector proc
while True:
try:
r = self._out_queue.get()
self._out_stream.append(r)
self._out_queue.task_done()
if len(self._out_stream) >= self._total:
print("Total {} jobs done.".format(len(self._out_stream)))
self._stop_event.set()
break
except Empty:
continue
def _serve(self):
for job in self._in_stream:
self._in_queue.put(job)
for _ in range(self._workers_num):
self._in_queue.put(None)
def _monitor(self):
running = 0
while True:
proc_name, job = self._run_queue.get()
running += 1
self._jobs_on_procs.update({proc_name: job})
self._run_queue.task_done()
if running == self._total:
break
class WorkerProcess(object):
def __init__(self, worker_id, in_queue, run_queue, out_queue, stop_event):
self._worker_id = worker_id
self._in_queue = in_queue
self._run_queue = run_queue
self._out_queue = out_queue
self._stop_event = stop_event
def run(self):
self._work()
print('worker - {} quit'.format(self._worker_id))
def _work(self):
print("worker - {0} start to work".format(self._worker_id))
job = {}
while not self._stop_event.is_set():
try:
job = self._in_queue.get(timeout=.01)
except Empty:
continue
if not job:
self._in_queue.task_done()
break
try:
proc = current_process()
self._run_queue.put((proc.name, job))
r = self._run_job(job)
self._out_queue.put(r)
except Exception as err:
print('Unhandle exception: {0}'.format(err), exc_info=True)
result = {"status": 'failed'}
self._out_queue.put(result)
finally:
self._in_queue.task_done()
def _run_job(self, job):
time.sleep(job)
return {
'status': 'succeed'
}
def main():
jobs = [3, 4, 5, 6, 7]
procs_num = 3
m = WorkersManager(jobs, procs_num)
m.run()
if __name__ == "__main__":
main()
問題代碼如下:
self._out_queue.put(result)
for _ in range(2):
# ISSUE HERE !!!
# NOTE: Call task_done twice
# Guessing:
# 1st time to swtich process?
# 2nd time to notify task has done?
# TODO: figure it out why?
self._in_queue.task_done()
我需要調用self._in_queue.task_done()
兩次以通知 JoinableQueue 作業已由異常處理邏輯完成。
我猜task_done()
第一次調用是否是切換進程上下文? 或其他任何東西。 根據測試。 第二個 task_done() 生效。
worker - 0 start to work
worker - 1 start to work
worker - 2 start to work
Process TIMEOUT: Process-1 5
Process TIMEOUT: Process-2 6
Process TIMEOUT: Process-3 7
Total 5 jobs done.
如果您調用 task_done() 一次,它將永遠阻塞並且不會完成。
問題是您有一個競爭條件,定義為:
當計算機程序要正確運行取決於程序進程或線程的順序或時間時,軟件中就會出現競爭條件。
在方法WorkerProcess._work
中,您的主循環開始:
while not self._stop_event.is_set():
try:
job = self._in_queue.get(timeout=.01)
except Empty:
continue
if not job:
self._in_queue.task_done()
break
self._stop_event
由_collect
線程設置。 根據發生這種情況時WorkerProcess._work
在循環中的位置,它可以退出循環,留下已放置在_in_queue
上的None
表示不再有作業。 顯然,這對於兩個進程發生了兩次。 它甚至可能發生在 0、1、2 或 3 個進程中。
修復方法是將while not self._stop_event.is_set():
替換為while True:
並僅依靠在_in_queue
上找到None
來表示終止。 這使您能夠為那些已正常完成的進程刪除對task_done
的額外調用(實際上,每個成功完成的進程只需要一個額外的調用,而不是您擁有的兩個)。
但這只是問題的一半。 另一半是你的代碼:
def _join_workers(self):
for p in self._spawned_procs:
p.join(TIMEOUT)
...
p.terminate()
因此,您沒有讓您的工作人員有足夠的時間來耗盡_in_queue
,因此可能會在其上留下任意數量的消息(在您的示例中,當然,只有當前的“工作”是處理和None
前哨共2)。
但這是代碼的一般問題:它被過度設計了。 例如,請參考上面的第一個代碼片段。 可以進一步簡化為:
while True:
job = self._in_queue.get() # blocking get
if not job:
break
此外,甚至沒有理由使用JoinableQueue
或Event
實例,因為使用放置在_in_queue
上的None
哨兵足以表示工作進程應該終止,特別是如果您要過早終止工作進程。 簡化的工作代碼是:
import time
import threading
from multiprocessing import Process, Queue, cpu_count, current_process
TIMEOUT = 3
class WorkersManager(object):
def __init__(self, jobs, processes_num):
self._processes_num = processes_num if processes_num else cpu_count()
self._workers_num = processes_num
self._in_queue, self._run_queue, self._out_queue = Queue(), Queue(), Queue()
self._spawned_procs = []
self._total = 0
self._jobs_on_procs = {}
self._wk_kwargs = dict(
in_queue=self._in_queue, run_queue=self._run_queue, out_queue=self._out_queue
)
self._in_stream = [j for j in jobs]
self._out_stream = []
self._total = len(self._in_stream)
def run(self):
# Spawn Worker
worker_processes = [
WorkerProcess(i, **self._wk_kwargs) for i in range(self._processes_num)
]
self._spawned_procs = [
Process(target=process.run, args=tuple())
for process in worker_processes
]
for p in self._spawned_procs:
p.start()
self._serve()
monitor = threading.Thread(target=self._monitor, args=tuple())
monitor.start()
collector = threading.Thread(target=self._collect, args=tuple())
collector.start()
self._join_workers()
# TODO: Terminiate threads
monitor.join()
collector.join()
return self._out_stream
def _join_workers(self):
for p in self._spawned_procs:
p.join(TIMEOUT)
if p.is_alive():
p.terminate()
job = self._jobs_on_procs.get(p.name)
print('Process TIMEOUT: {0} {1}'.format(p.name, job))
result = {
"status": "failed"
}
self._out_queue.put(result)
else:
if p.exitcode == 0:
print("{} exit with code:{}".format(p, p.exitcode))
else:
job = self._jobs_on_procs.get(p.name)
if p.exitcode > 0:
print("{} with code:{} {}".format(p, p.exitcode, job))
else:
print("{} been killed with code:{} {}".format(p, p.exitcode, job))
result = {
"status": "failed"
}
self._out_queue.put(result)
def _collect(self):
# TODO: Spawn a collector proc
while True:
r = self._out_queue.get()
self._out_stream.append(r)
if len(self._out_stream) >= self._total:
print("Total {} jobs done.".format(len(self._out_stream)))
break
def _serve(self):
for job in self._in_stream:
self._in_queue.put(job)
for _ in range(self._workers_num):
self._in_queue.put(None)
def _monitor(self):
running = 0
while True:
proc_name, job = self._run_queue.get()
running += 1
self._jobs_on_procs.update({proc_name: job})
if running == self._total:
break
class WorkerProcess(object):
def __init__(self, worker_id, in_queue, run_queue, out_queue):
self._worker_id = worker_id
self._in_queue = in_queue
self._run_queue = run_queue
self._out_queue = out_queue
def run(self):
self._work()
print('worker - {} quit'.format(self._worker_id))
def _work(self):
print("worker - {0} start to work".format(self._worker_id))
job = {}
while True:
job = self._in_queue.get()
if not job:
break
try:
proc = current_process()
self._run_queue.put((proc.name, job))
r = self._run_job(job)
self._out_queue.put(r)
except Exception as err:
print('Unhandle exception: {0}'.format(err), exc_info=True)
result = {"status": 'failed'}
self._out_queue.put(result)
def _run_job(self, job):
time.sleep(job)
return {
'status': 'succeed'
}
def main():
jobs = [3, 4, 5, 6, 7]
procs_num = 3
m = WorkersManager(jobs, procs_num)
m.run()
if __name__ == "__main__":
main()
印刷:
worker - 0 start to work
worker - 1 start to work
worker - 2 start to work
Process TIMEOUT: Process-1 3
Process TIMEOUT: Process-2 6
Process TIMEOUT: Process-3 7
Total 5 jobs done.
您可能知道這一點,但盡職調查要求我提到有兩個出色的類multiprocessing.Pool
和concurrent.futures.ProcessPoolExecutor
可以完成您想要完成的工作。 請參閱此進行一些比較。
進一步說明
使用支持調用task_done
的JoinableQueue
有什么意義? 通常,這是為了確保您放置在隊列中的所有消息都已從隊列中取出並進行處理,並且主進程不會在此之前提前終止。 但這在您擁有的代碼中無法正常工作,因為您只給進程提供了TIMEOUT
秒來處理其消息,然后如果它仍然存在並且消息可能仍留在其隊列中,則終止該進程。 這就是迫使您人為地向task_done
發出額外調用的原因,這樣您在主進程中join
隊列的調用就不會掛起,也是您必須首先發布此問題的原因。
因此,您可以通過兩種不同的方式進行操作。 一種方法是允許您繼續使用JoinableQueue
實例並在這些實例上調用join
以了解何時終止。 但是(1)您將無法過早終止您的消息進程,並且(2)您的消息進程必須正確處理異常,以便它們不會在不清空隊列的情況下過早終止。
另一種方式是我提出的,更簡單。 主進程只是在輸入隊列上放置一個特殊的標記消息,在本例中為None
。 這只是一條消息,不能被誤認為是要處理的實際消息,而是表示文件結束,或者換句話說,向消息進程發出一個信號,表明隊列中沒有更多消息,它可能現在終止。 因此,除了要在隊列上處理的“真實”消息之外,主進程只需要放置額外的哨兵消息,然后在消息隊列上進行join
調用(現在只是常規的、不可加入的) queues),它會在每個流程實例上join(TIMEOUT)
,你會發現它不再活着,因為它已經看到了哨兵,因此你知道它已經處理了所有的消息,或者你可以在進程上調用terminate
如果您願意在其輸入隊列上留下消息。
當然,要真正確定自行終止的進程確實清空了它們的隊列,可能需要您檢查它們的隊列以查看它們確實是空的。 但是我假設您應該能夠對您的流程進行編碼以正確處理異常,至少是那些可以處理的異常,以便它們不會過早終止並對每條消息執行“合理”的操作。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.