文章目錄
- 一、簡介
- 二、選擇合適的啟動方式
- 三、手動終止所有的進程
- 小結
一、簡介
這里簡單介紹在Python中使用多進程編程的時候容易遇到的情況和解決辦法,有助于排查和規避某類問題,但是具體問題還是需要具體分析,后續會補充更多的內容。
二、選擇合適的啟動方式
-
Python 的多進程編程非常需要注意啟動方式,有些庫會在
fork
模式下出現很詭異的阻塞或顯存泄漏。如果需要跨平臺使用,優先選擇spawn
方式,如果涉及到 GPU、PyTorch、OpenCV 視頻流、TensorRT、OpenVINO ,同樣優先選擇spawn
方式,避免繼承GPU上下文。有的時候會存在 多線程 和 多進程 混用的情況,可以選擇spawn
或forkserver
這兩種方式。spawn
啟動方式通常更安全、更穩定。啟動方式 平臺默認 機制 優點 缺點 適用場景 fork Linux / macOS 子進程直接復制父進程的內存空間(寫時復制),從 fork()
返回處繼續執行創建速度快,占用內存少 繼承父進程線程狀態可能死鎖,繼承 GPU/文件句柄可能出錯,不支持 Windows 純計算、多進程間狀態共享簡單、無 GPU/大文件句柄的場景 spawn Windows 啟動全新的 Python 解釋器進程,從頭執行 __main__
模塊的代碼跨平臺一致,狀態干凈,不繼承父進程線程/句柄/GPU context 啟動慢,初始化開銷大,需要 if __name__ == "__main__":
GPU 深度學習、跨平臺項目、多線程+多進程混合、需要隔離狀態的場景 forkserver Linux / macOS 先啟動一個 “fork server” 進程,之后所有子進程由它 fork
出來避免從主進程直接 fork(減少死鎖風險),比 spawn 快 只在類 Unix 系統可用,需額外啟動 server,復雜度高 多線程+多進程并存、需要避免主進程 fork 的高并發服務 -
可以通過調用
multiprocessing.set_start_method('spawn')
來指定子進程的啟動方式,通常寫在if __name__ == "__main__":
里,force=True
參數可以強制重置啟動方式(但通常不建議隨便用,除非確定沒別的進程創建了子進程)。import multiprocessing as mpdef worker(name):print(f"Worker {name} running...")if __name__ == "__main__":# 強制統一 spawnmp.set_start_method("spawn", force=True) # "fork" 或 "forkserver" processes = []for i in range(3):p = mp.Process(target=worker, args=(i,))p.start()processes.append(p)for p in processes:p.join()
-
使用 PyAV 調用
h264_qsv
(Intel Quick Sync Video)硬件加速解碼時,也會受多進程啟動方式的影響,QSV 是基于英特爾硬件加速的編碼解碼技術,需要在進程中正確初始化硬件上下文。如果使用fork
,子進程會復制父進程的內存空間,但硬件設備上下文(如 QSV 的硬件句柄、驅動狀態等)通常不能被正確繼承或共享,導致解碼失敗、死鎖或崩潰。使用spawn
,子進程是全新啟動,獨立初始化硬件上下文,能避免因上下文繼承帶來的問題,提高穩定性。多進程啟動方式 對 PyAV + h264_qsv 硬解碼的影響 推薦做法 fork 可能導致硬件上下文沖突,解碼異常 不推薦 spawn 子進程獨立初始化硬件上下文,穩定性高 推薦,尤其多進程并發的時候 -
在使用
fork
啟動多進程的時候,部分常用的 計算機視覺 (CV)和 點云處理 相關庫可能會出現問題,任何涉及 GPU上下文、線程池或多線程加速的庫,在fork
多進程啟動方式下都可能出現資源繼承異常、死鎖、崩潰等問題。點云庫中,Open3D
和PCL
較為復雜,fork
時要特別注意。庫/模塊 說明與常見問題 OpenCV (cv2) GPU模式(CUDA)在fork后可能資源異常或崩潰,多線程環境下死鎖;CPU模式一般安全,但多線程仍需注意。 Open3D 內部使用線程池和GPU(部分功能),fork啟動時可能導致線程池狀態異常或GPU資源不可用。 PCL / python-pcl 底層多線程和GPU支持(部分平臺),fork后可能出現線程死鎖或資源沖突,尤其在GPU加速時。 PyTorch fork后CUDA上下文繼承出錯,導致報錯、卡死;多線程資源也可能異常。 TensorFlow fork后session、資源無法正常初始化,多線程管理導致崩潰。 OpenVINO 線程池和設備初始化受fork影響,可能導致設備資源沖突或初始化失敗。 TensorRT GPU上下文和優化緩存fork后不穩定,可能導致初始化失敗或運行錯誤。 matplotlib GUI線程在fork后可能卡死或異常。
三、手動終止所有的進程
-
多進程中
Ctrl+C
(SIGINT
信號)后程序沒有全部退出,最常見原因就是信號沒有傳遞給子進程,或者子進程沒能響應退出請求。fork
導致資源繼承異常也經常是根源。列出當前系統中所有正在運行的 Python 進程及其詳細信息。ps aux | grep python
-
在多進程場景下,
torch.utils.data.DataLoader
是常見的導致進程無法退出的元兇之一,尤其是在num_workers > 0
時。有的情況默認不監聽結束信號,有的情況收不到結束信號。顯式處理可能比較麻煩。 -
一個比較安全的寫法是在主進程捕獲
SIGINT
,然后主動殺掉自己啟動的所有子進程。直接一次性結束整個進程樹。import multiprocessing as mp import signal import sys import os import psutildef kill_all_children(timeout=3):"""終止當前進程的所有子進程(遞歸),先嘗試終止,再強制殺死超時未結束的子進程"""proc = psutil.Process(os.getpid())children = proc.children(recursive=True)# 嘗試終止for child in children:try:child.terminate()except Exception as e:print(f"[警告] 無法終止 PID={child.pid}: {e}", file=sys.stderr)# 等待超時,獲取仍然存活的子進程gone, alive = psutil.wait_procs(children, timeout=timeout)# 強制殺死仍然存活的進程for child in alive:try:child.kill()except Exception as e:print(f"[錯誤] 無法強制終止 PID={child.pid}: {e}", file=sys.stderr)def signal_handler(sig, frame): # 回調函數print(f"\n[退出] 捕獲信號 {sig},終止子進程并退出...")try:kill_all_children()except Exception as e:print(f"[錯誤] 終止子進程時出錯: {e}", file=sys.stderr)finally:sys.exit(130 if sig == signal.SIGINT else 143) # 130 = Ctrl+C 退出, 143 = SIGTERM 退出def main(args):# 啟動多個進程的示例processes = []for _ in range(4):p = mp.Process(target=worker_function)p.start()processes.append(p)for p in processes:p.join()if __name__ == "__main__":signal.signal(signal.SIGINT, signal_handler) # 注冊信號和回調函數signal.signal(signal.SIGTERM, signal_handler)mp.set_start_method('spawn', force=True)args = parse_args()main(args)
-
或者更簡單直接的立即殺掉所有進程,終端更干凈。可以根據實際情況考慮如何使用。
import multiprocessing as mp import signal import sys import os import psutildef kill_all_processes():proc = psutil.Process(os.getpid())for child in proc.children(recursive=True):child.kill()proc.kill()def signal_handler(sig, frame):print("\n[退出] 捕獲 Ctrl+C,終止所有進程...")kill_all_processes()sys.exit(0)def main(args):# 啟動多個進程的示例processes = []for _ in range(4):p = mp.Process(target=worker_function)p.start()processes.append(p)for p in processes:p.join()if __name__ == "__main__":signal.signal(signal.SIGINT, signal_handler)signal.signal(signal.SIGTERM, signal_handler)mp.set_start_method('spawn', force=True)args = parse_args()main(args)
小結
以上內容來自相關資料和個人實踐,具體情況還請具體分析,可以參考這里提到的幾個關鍵點,有助于排查問題,如有問題歡迎在評論區指正,謝謝!!