2012-07-03 170 views
59

如何在多進程python程序中捕獲Ctrl + C並正常退出所有進程,我需要解決方案在unix和windows上都能正常工作。我已經試過如下:抓住Ctrl + C/SIGINT並在python中優雅地退出多進程

import multiprocessing 
import time 
import signal 
import sys 

jobs = [] 

def worker(): 
    signal.signal(signal.SIGINT, signal_handler) 
    while(True): 
     time.sleep(1.1234) 
     print "Working..." 

def signal_handler(signal, frame): 
    print 'You pressed Ctrl+C!' 
    # for p in jobs: 
    #  p.terminate() 
    sys.exit(0) 

if __name__ == "__main__": 
    for i in range(50): 
     p = multiprocessing.Process(target=worker) 
     jobs.append(p) 
     p.start() 

而且這樣的工作,但我不認爲這是正確的解決方案。

編輯: 這可能是this one

+1

可能的重複[鍵盤中斷與python的多處理池](https://stackoverflow.com/questions/1408356/keyboard-interrupts-with-pythons-multiprocessingpool) – tabata

回答

37

The previously accepted solution有競爭條件,它不起作用mapasync函數。

處理正確的方法按Ctrl + C/SIGINTmultiprocessing.Pool是:

  1. 使這一進程創建進程Pool之前忽略SIGINT。這種方式創建的子進程繼承了SIGINT處理程序。
  2. 在創建了Pool之後,在父進程中恢復原始SIGINT處理程序。
  3. 使用map_asyncapply_async而不是阻止mapapply
  4. 等待結果超時,因爲默認阻止等待會忽略所有信號。這是Python bug https://bugs.python.org/issue8296

將其組合在一起:

#!/bin/env python 
from __future__ import print_function 

import multiprocessing 
import os 
import signal 
import time 

def run_worker(delay): 
    print("In a worker process", os.getpid()) 
    time.sleep(delay) 

def main(): 
    print("Initializng 2 workers") 
    original_sigint_handler = signal.signal(signal.SIGINT, signal.SIG_IGN) 
    pool = multiprocessing.Pool(2) 
    signal.signal(signal.SIGINT, original_sigint_handler) 
    try: 
     print("Starting 2 jobs of 5 seconds each") 
     res = pool.map_async(run_worker, [5, 5]) 
     print("Waiting for results") 
     res.get(60) # Without the timeout this blocking call ignores all signals. 
    except KeyboardInterrupt: 
     print("Caught KeyboardInterrupt, terminating workers") 
     pool.terminate() 
    else: 
     print("Normal termination") 
     pool.close() 
    pool.join() 

if __name__ == "__main__": 
    main() 

如@YakovShklarov指出,存在的時間忽略的信號,並在父進程,在此期間,可以將信號丟失它取消忽略之間的窗口。改爲使用pthread_sigmask來臨時阻止父進程中的信號傳遞將阻止信號丟失,但是它在Python-2中不可用。

+0

儘管SIGINT引發了一個很長的回溯,但這個答案在Python 2.7.11中適用。在3.5.2中,這個答案不起作用 - SIGINT永遠不會正常退出。相反,您可以從多處理模塊獲得'KeyboardInterrupt'和'Process SpawnPoolWorker'消息和回溯,並且必須手動終止衍生進程。接受的答案不表現出這種行爲。 – MartyMacGyver

+0

@MartyMacGyver只用Python 2.7.x測試過。 –

+0

也適用於Linux(Ubuntu 16.04)和Python 2.7.11。這真的應該是被接受的答案。 – theV0ID

33

重複的解決方案是基於​​和this link,它解決了這個問題,我不得不搬到Pool雖然:

import multiprocessing 
import time 
import signal 
import sys 

def init_worker(): 
    signal.signal(signal.SIGINT, signal.SIG_IGN) 

def worker(): 
    while(True): 
     time.sleep(1.1234) 
     print "Working..." 

if __name__ == "__main__": 
    pool = multiprocessing.Pool(50, init_worker) 
    try: 
     for i in range(50): 
      pool.apply_async(worker) 

     time.sleep(10) 
     pool.close() 
     pool.join() 

    except KeyboardInterrupt: 
     print "Caught KeyboardInterrupt, terminating workers" 
     pool.terminate() 
     pool.join() 
+0

這有點太晚了:有一場比賽'fork()'返回子進程和'signal()'調用之間的條件窗口。在分叉之前信號必須被阻塞。 –

+1

@MaximYegorushkin - 信號在'apply_async'之前調用的'init_worker'中被阻塞 - 是你在說什麼? – zenpoy

+0

我的意思是信號必須在子進程分叉和解除阻塞之前被阻塞。這樣子女就會繼承信號掩碼並且沒有機會接收信號。 –

11

剛在您的工作進程中處理KeyboardInterrupt-SystemExit異常:

def worker(): 
    while(True): 
     try: 
      msg = self.msg_queue.get() 
     except (KeyboardInterrupt, SystemExit): 
      print("Exiting...") 
      break 
+0

對於使Python升級SystemExit的信號,這確實也適用於Python 3.6。我想知道,這包括什麼信號?我會猜測SIGKILL和SIGTERM ...? – Petri

+1

你可以很容易地檢查哪些信號包括答案是:我認爲沒有。 SystemExit僅由sys.exit根據文檔引發。只要執行'try:time.sleep(60),除了BaseException as e:print(e)',你就會看到是否捕獲到一個特定的信號(ime只有SIGINT)。這也是手冊頁所說的。 –