python 2.7: 如何在超过25个线程的程序中捕获键盘中断

python 2.7: how to catch keyboard interrupt in program with more than 25 threads

我想在用户按下 ctrl-C 时停止我的程序。 以下答案建议捕获 KeyboardInterrupt 异常。

python: how to terminate a thread when main program ends

有时有效。但是在下面的例子中,当我将线程数从 25 增加到 30 后,它停止工作了。

import threading, sys, signal, os

stderr_lock = threading.Lock()

def Log(module, msg):
    with stderr_lock:
        sys.stderr.write("%s: %s\n" % (module, msg))

class My_Thread(threading.Thread):
    def __init__(self):
        threading.Thread.__init__(self)
        Log("Init", "Initing.")
        self.start()
    def run(self):
        try:
            while True:
                Log("Run", "Running.")
        except KeyboardInterrupt:
            os._exit(0)

for i in range(30):
    My_Thread()

# trap ctrl-C in main thread
try:
    while True:
        pass
except KeyboardInterrupt:
    os._exit(0)

这与以下问题有非常相似的感觉:

在这种情况下,我在将线程数增加到 87 以上后无法捕获信号。

您可能想阅读,即:

There are 3 exit functions, in addition to raising SystemExit.

The underlying one is os._exit, which requires 1 int argument, and exits immediately with no cleanup. It's unlikely you'll ever want to touch this one, but it is there.

sys.exit is defined in sysmodule.c and just runs PyErr_SetObject(PyExc_SystemExit, exit_code);, which is effectively the same as directly raising SystemExit. In fine detail, raising SystemExit is probably faster, since sys.exit requires an LOAD_ATTR and CALL_FUNCTION vs RAISE_VARARGS opcalls. Also, raise SystemExit produces slightly smaller bytecode (4bytes less), (1 byte extra if you use from sys import exit since sys.exit is expected to return None, so includes an extra POP_TOP).

The last exit function is defined in site.py, and aliased to exit or quit in the REPL. It's actually an instance of the Quitter class (so it can have a custom repr, so is probably the slowest running. Also, it closes sys.stdin prior to raising SystemExit, so it's recommended for use only in the REPL.

As for how SystemExit is handled, it eventually causes the VM to call os._exit, but before that, it does some cleanup. It also runs atexit._run_exitfuncs() which runs any callbacks registered via the atexit module. Calling os._exit directly bypasses the atexit step.

因此,raise SystemExit 可能是捕获到异常时退出的首选方式。

您的代码实际上有两个不同的问题导致了这种行为。第一是你的线程应该做成daemon线程,这样主线程退出的时候自动停止,第二是你的try块没有封装线程的创建和启动。

当您创建多个线程时,线程创建不会完成很长一段时间(因为它会不断被创建的线程中断并且 GIL 阻止它们 运行在平行下)。因此,您在设置待处理之前发送您的 KeyboardInterrupt。但是,KeyboardInterrupt 仍会终止主线程(使用 Traceback),但不会终止子线程。

因此,如果您将代码修改为:

import threading, sys, signal, os

stderr_lock = threading.Lock()

def Log(module, msg):
    with stderr_lock:
        sys.stderr.write("%s: %s\n" % (module, msg))

class My_Thread(threading.Thread):
    def __init__(self, value):
        threading.Thread.__init__(self)
        self.value = value
        Log("Init", "Initing %d." % self.value)
        self.daemon = True
        self.start()
    def run(self):
        while True:
            Log("Run", "Running %d." % self.value)

# trap ctrl-C in main thread
try:
    for i in range(1000):
        My_Thread(i)

    while True:
        pass
except KeyboardInterrupt:
    os._exit(0)

请注意,在当前示例中,将线程变成守护进程并不是绝对必要的,但我认为这对于应该在主程序结束时结束的线程来说是一种很好的做法。