多线程忽略 KeyboardInterrupt 异常

67

我正在运行这段简单的代码:

import threading, time

class reqthread(threading.Thread):    
    def run(self):
        for i in range(0, 10):
            time.sleep(1)
            print('.')

try:
    thread = reqthread()
    thread.start()
except (KeyboardInterrupt, SystemExit):
    print('\n! Received keyboard interrupt, quitting threads.\n')

但是当我运行它时,它打印出

$ python prova.py
.
.
^C.
.
.
.
.
.
.
.
Exception KeyboardInterrupt in <module 'threading' from '/usr/lib/python2.6/threading.pyc'> ignored

实际上,Python线程忽略了我的Ctrl+C键盘中断,并且没有打印Received Keyboard Interrupt。为什么?这段代码有什么问题?

6个回答

78

尝试

try:
    thread = reqthread()
    thread.daemon = True
    thread.start()
    while True:
        time.sleep(100)
except (KeyboardInterrupt, SystemExit):
    print('Received keyboard interrupt, quitting threads.')
 

如果没有调用 time.sleep,主进程会在太早的时候跳出 try...except 块,因此 KeyboardInterrupt 没有被捕获。我最初的想法是使用 thread.join,但似乎会阻塞主进程(无视 KeyboardInterrupt),直到 thread 完成。

thread.daemon=True 会导致线程在主进程结束时终止。


7
我相信在 join 上设置超时,即 while thread.isAlive: thread.join(5),也可以使主线程对异常保持响应。 - Dr. Jan-Philip Gehrcke
14
thread.daemon = True 实际上不建议使用,因为它会防止线程清理任何遗留的资源... - Erik Kaplun

14

总结评论中建议的更改对我而言以下方法有效:

try:
  thread = reqthread()
  thread.start()
  while thread.isAlive(): 
    thread.join(1)  # not sure if there is an appreciable cost to this.
except (KeyboardInterrupt, SystemExit):
  print '\n! Received keyboard interrupt, quitting threads.\n'
  sys.exit()

就我个人而言,我不知道;如果性能对你很重要的话,也许值得尝试制作一个基准测试? - rattray
1
请注意,exit() 和 sys.exit() 不是相同的。建议使用 sys.exit()。 - DevPlayer
感谢 @DevPlayer!已更新以反映此事。对于好奇的人,请参见 https://dev59.com/xGIj5IYBdhLWcg3w8JNZ#19747562 以获取解释。 - rattray
1
对我不起作用。它打印“接收到键盘中断,退出线程。”但线程仍在打印点。 - Fabian
1
isAlive 要么不再使用,要么已重命名为 "is_alive"。我和 @fabian 有同样的问题,线程一直在运行。 - TheLexoPlexx
显示剩余2条评论

9

对Ubuntu解决方案进行轻微修改。

根据Eric的建议,删除tread.daemon = True并将睡眠循环替换为signal.pause():

import signal

try:
  thread = reqthread()
  thread.start()
  signal.pause()  # instead of: while True: time.sleep(100)
except (KeyboardInterrupt, SystemExit):
  print('Received keyboard interrupt, quitting threads.)

6
很好 - 但不幸的是它不支持Windows。 - Tobias Kienzler

0

我的(hacky)解决方案是像这样猴子补丁Thread.join()

def initThreadJoinHack():
  import threading, thread
  mainThread = threading.currentThread()
  assert isinstance(mainThread, threading._MainThread)
  mainThreadId = thread.get_ident()
  join_orig = threading.Thread.join
  def join_hacked(threadObj, timeout=None):
    """
    :type threadObj: threading.Thread
    :type timeout: float|None
    """
    if timeout is None and thread.get_ident() == mainThreadId:
      # This is a HACK for Thread.join() if we are in the main thread.
      # In that case, a Thread.join(timeout=None) would hang and even not respond to signals
      # because signals will get delivered to other threads and Python would forward
      # them for delayed handling to the main thread which hangs.
      # See CPython signalmodule.c.
      # Currently the best solution I can think of:
      while threadObj.isAlive():
        join_orig(threadObj, timeout=0.1)
    else:
      # In all other cases, we can use the original.
      join_orig(threadObj, timeout=timeout)
  threading.Thread.join = join_hacked

0

在每个线程中放置try ... except,并在truemain()中也加入signal.pause()对我很有效。

注意import lock。我猜这就是为什么Python默认情况下无法解决ctrl-C的原因。


0

我知道这已经很旧了,但我遇到了完全相同的问题,并且需要在Docker(ubuntu 20.04)和Windows上使Ctrl-C行为正常工作。特别是在Windows上,信号处理只在主线程上进行,在线程不处于等待状态时才会执行。对于try: except KeyboardInterrupt:以及signal.signal(signal.SIGINT, handler)两种情况,只有当主线程不处于等待状态时才会触发或调用。

例如,如果您将代码更改为以下内容,并在中途按下Ctrl-C,您将看到异常被捕获,但仅在reqThread实际终止并且thread.join()返回时才会发生。

import threading, time

class reqthread(threading.Thread):
    def run(self):
        for i in range(0, 10):
            time.sleep(1)
            print('.')

try:
    thread = reqthread()
    thread.start()
    thread.join()
except (KeyboardInterrupt, SystemExit):
    print('\n! Received keyboard interrupt, quitting threads.\n')

然而,有趣的是,当主线程运行一个asyncio循环时,在Windows和Linux上(至少在我正在使用的docker Ubuntu镜像上),它总是能够捕获到Ctrl-C信号。
以下代码段演示了这种行为。
import threading, time, signal, asyncio

localLoop = asyncio.new_event_loop()
syncShutdownEvent = threading.Event()

class reqthread(threading.Thread):
    def run(self):
        for i in range(0, 10):
            time.sleep(1)
            print('.')
            if syncShutdownEvent.is_set():
                break

        print("reqthread stopped")

        done()

        return

def done():
    localLoop.call_soon_threadsafe(lambda: localLoop.stop())

def handler(signum, frame):
    signal.getsignal(signum)
    print(f'\n! Received signal {signal.Signals(signum).name}, quitting threads.\n')
    syncShutdownEvent.set()

def hookKeyboardInterruptSignals():
    for selectSignal in [x for x in signal.valid_signals() if isinstance(x, signal.Signals) and x.name in ('SIGINT', 'SIGBREAK')]:
        signal.signal(selectSignal.value, handler)

hookKeyboardInterruptSignals()
thread = reqthread()
thread.start()
asyncio.set_event_loop(localLoop)
localLoop.run_forever()
localLoop.close()

并且将在Windows和Ubuntu上具有相同的行为

python scratch_14.py
.
.

! Received keyboard interrupt, quitting threads.

.
reqthread stopped

对于我的特定应用,需要使用一个运行同步代码的线程和一个运行异步代码的线程,我实际上使用了总共三个线程。

  1. 主线程,运行Ctrl-C asyncio拦截器
  2. 同步线程
  3. Asyncio循环线程

编辑:修复了一个错别字,导致第一个代码块的import语句被解释为普通文本而不是代码块的一部分


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接