Start with an example

A piece of code

先看一段代码:

from multiprocessing import Process, Manager
from time import sleep

def f(process_number):
    try:
        print "starting thread: ", process_number
        while True:
            print process_number
            sleep(3)
    except KeyboardInterrupt:
        print "Keyboard interrupt in process: ", process_number
    finally:
        print "cleaning up thread", process_number

if __name__ == '__main__':
    processes = []
    manager = Manager()
    for i in xrange(4):
        p = Process(target=f, args=(i,))
        p.start()
        processes.append(p)

    try:
        for process in processes:
            process.join()
    except KeyboardInterrupt:
        print "Keyboard interrupt in main"
    finally:
        print "Cleaning up Main"

运行后按ctrl-c,可以看到:

^C
Keyboard interrupt in process:  3
Keyboard interrupt in process:  0
Keyboard interrupt in process:  2
cleaning up thread 3
cleaning up thread 0
cleaning up thread 2
Keyboard interrupt in process:  1
cleaning up thread 1
Keyboard interrupt in main
Cleaning up Main

可以看到这里利用KeyboardInterrupt使所有进程都得以比较优雅地退出。这种退出进程的方法确实值得学习,但它仅限于进程是通过ctrl-c的方式结束的情况,假如我调用的是Process.terminate()方法来结束这些进程的话,该如何优雅地退出呢?

What just happened

我们先要弄明白上面的这段代码到底发生了什么:

  • 当我们按下ctrl-c后会在主进程触发一个signal.SIGINT信号,然后主进程会把这个信号逐一发布给其所有的子进程(Signals are propagated down the process tree)。
  • signal.SIGINT信号default就绑定了一个handler方法,这个handler方法会raise KeyboardInterrupt。所以所有的进程都会产生一个KeyboardInterrupt异常。(换个角度理解,default绑定的handler方法就像是操作系统的预安装的软件,在你起任何一个python进程的时候就执行了绑定的代码。)
  • 我们抓住了KeyboardInterrupt异常,然后就可以做进程退出前的一些工作了。

The signal machinism in Python

Signal handler

信号的handler方法其实是一个回调函数,触发了某个信号后才会去调用对应handler方法。(可以理解为有一个线程会专门接收和处理信号,一旦这个线程接收到信号就会调用其handler方法来处理)

Python标准库中提供了两个现成的handler方法:signal.SIG_DFLsignal.SIG_IGN。前者是使用每个信号default的handler方法(各个信号不一样),后者是ignore这个信号,即绑定了这个handler后,接收到此信号就和没接收到时一样。

Write a signal handler

一个signal的handler必须要有两个输入参数,第一个是信号对应的数值,第二个是接收到信号时stack的信息。写好handler后要通过signal.signal()来绑定signal和handler方法。

例子如下:

import signal

def signal_handler(signum, stack):
    print 'Received:', signum

signal.signal(signal.SIGINT, signal_handler)

if __name__ == '__main__':
    while True:
        print 'Waiting...try kill using signal 2(SIGINT)'
        time.sleep(3)

需要注意的是一旦绑定好handler之后,从主进程创建的子进程中此信号和此handler也是绑定的(因为是从主进程fork出来的进程嘛)。

另外,SIGKILLSIGSTOP是不能绑定handler的(cannot be caught, blocked, or ignored)。

Understand from another angle

用Pycharm来run上述的代码,然后强制Stop(⌘F2)程序。你会发现按一次Stop按钮后,屏幕上输出了:

Waiting...try kill using signal 2(SIGINT)
Received: 2
Waiting...try kill using signal 2(SIGINT)
Waiting...try kill using signal 2(SIGINT)
Waiting...try kill using signal 2(SIGINT)

程序并没有结束,而绑定的你的signal handler被调用了一次。说明第一次Stop,Pycharm是发送了SIGINT到主进程(就和你执行程序时按ctrl-c一样)。而后再按一次Stop按钮,程序直接就结束了。这一次Stop,Pycharm是发送SIGKILL到主进程,进程直接被强制kill掉了。正是由于SIGKILL无法绑定handler,从而保证了进程可以被杀死。

Exit the process gracefully with other signals

参考前文的ctrl-c的例子,利用异常也可以让Process.terminate()来优雅地结束子进程:

from multiprocessing import Process
import signal
import time


class TerminateInterrupt(BaseException): pass


def signal_handler(signum, stack):
    print 'Capture terminate signal.'
    raise TerminateInterrupt


class MyProcess(Process):
    def __init__(self):
        super(MyProcess, self).__init__()
        self.a = 0

    def run(self):
        signal.signal(signal.SIGTERM, signal_handler)
        try:
            while True:
                self.a += 1
                time.sleep(1)
        except TerminateInterrupt:
            print 'Exit the process.'


if __name__ == '__main__':
    p = MyProcess()
    p.start()
    time.sleep(5)
    p.terminate()
    p.join()

执行上述代码结果为:

Capture terminate signal.
Exit the process.

注意这里是在run方法里面对signal.SIGTERM信号进行的绑定,因为run方法里面已经是子进程的空间了,如果是在__init__方法做这个绑定的话,主进程其实也被一并绑定了(这样子进程是通过fork自然拥有这个绑定的)。

由于信号的处理是异步回调的操作,所以使用异常来通知进程结束是最为方便的做法。不使用异常也是可以的,比如设一个变量表示是否结束,while语句中需要对这个变量进行判断,而signal handler用来修改这个变量的值即可(这种在子进程中每隔一段时间轮询是否退出的方式要更加优雅一些,这样保证了一个操作轮回的结束,而使用信号产生异常的方式则很可能在操作当中就直接结束了,导致操作只进行了一半(想象下如果这个操作是发送一条数据,发送的数据不完整是很致命的))。

Reference

  1. Python | Multiprocessing and Interrupts
  2. Safe use of unix signals with multiprocessing module in python
  3. Python - Signal handling and identifying stack frame