[英]SystemExit deadlock with multiprocessing + asyncio
在 asyncio 任務中引發SystemExit
時,以下代碼會死鎖而不是退出。
import asyncio
import multiprocessing as mp
def worker_main(pipe):
try:
print("Worker started")
pipe.recv()
finally:
print("Worker exiting.")
async def main():
_, other_end = mp.Pipe()
worker = mp.Process(target=worker_main, args=(other_end,))
worker.daemon = False # (default), True causes multiprocessing explicitly to terminate worker
worker.start()
await asyncio.sleep(2)
print("Main task raising SystemExit")
raise SystemExit
if __name__ == "__main__":
asyncio.get_event_loop().run_until_complete(main())
它僅打印以下內容然后掛起:
Worker started
Main task raising SystemExit
如果我然后按 Ctrl+CI 得到一個回溯,表明它正在等待 multiprocessing.util._exit_function() 中的p.join()
multiprocessing.util._exit_function()
。
這是 Python 3.9.5 上的 Windows 10。
從表面上看,原因似乎如下:
當在異步任務中引發SystemExit
時,似乎所有任務都已結束,然后SystemExit
在事件循環之外重新引發。 多處理庫已使用atexit.register()
注冊multiprocessing.util._exit_function()
) ,並在主進程退出時調用它。 這將在工作進程上執行p.join()
。 至關重要的是,這發生在pipe 關閉之前,因此它與等待 pipe 關閉 ( EOFError
) 的工作人員死鎖,而主進程等待工作人員退出。
解決方案/解決方法似乎是讓工作人員成為守護進程,以便_exit_function()
將在p.join()
之前明確終止它,從而打破僵局。 唯一的問題是它會阻止工作人員在退出之前進行任何清理工作。
在非 asyncio 應用程序中不會出現同樣的問題,我不確定為什么會有所不同。 如果應用程序是非異步的,那么當主進程退出 pipe 到 worker 時,worker 將按預期退出並出現EOFError
。 我還確認如果 asyncio 任務被允許正常退出,但在run_until_complete()
返回后有一個raise SystemExit
那么它的行為與非 asyncio 情況一樣 - 即它正確退出。
這是 Python 中的一個錯誤,還是它應該以這種方式運行?
Python3.10.0 + Win10 具有相同的行為。
這是 Python 中的一個錯誤,還是預期會這樣?
好問題。 在我看來,這不是 Python 中的錯誤。您有一個具有單個任務和單個線程的 asyncio 程序。 一旦啟動事件循環,主線程中的所有代碼都在事件循環中運行。 當它執行p.join()
時,它是一個阻塞調用。 由於沒有第二個線程可以導致解除阻塞,因此程序會在此時掛起。
據我了解,asyncio 程序中的每個任務都會在(可能)傳播異常之前處理自己的異常,通常傳播到 asyncio.run() 調用。 (至少這是我的經驗,否則很難看出它會怎樣。)所以異常處理環境在同步和異步程序之間是不同的。
第二點:Python 不可能知道如何優雅地終止非守護進程。 如果您將進程指定為守護進程,您實際上是在告訴 Python 該進程可以安全終止。 但是,正如您所指出的那樣,這將繞過所需的任何清理工作。
由於您有一個非守護進程,因此由您的代碼執行清理。
第三點:如果你更換這一行:
raise SystemExit
有了這個:
raise Exception
出現完全相同的問題,並且出於相同的原因。 該程序不會退出,因為有一個非守護進程正在運行。 問題不僅僅是從 SystemExit 中清理,而是從任何異常中清理。 如果你解決了那個問題,你的問題就沒有實際意義了。
您可以在主 function 中顯式捕獲 SystemExit 和 Exception,如下所示:
import asyncio
import multiprocessing as mp
def worker_main(pipe):
try:
print("Worker started")
pipe.recv()
finally:
print("Worker exiting.")
async def main():
my_end, other_end = mp.Pipe()
worker = mp.Process(target=worker_main, args=(other_end,))
try:
worker.daemon = False
worker.start()
await asyncio.sleep(2)
print("Main task raising SystemExit")
raise SystemExit
# raise Exception
except (SystemExit, Exception):
my_end.send("Bye")
worker.join()
raise
if __name__ == "__main__":
asyncio.run(main())
您可以通過注釋掉一個或另一個來了解 SystemExit 和 Exception 之間的區別。 兩人優雅地退出。 SystemExit 不會將回溯打印到控制台; 例外。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.