解决Ctrl+C无法退出python程序的问题

emmm

最近在帮 @XaioeyuyoQWQ 的时候遇到了一个问题,在程序跑起来之后,Ctrl+C直接什么反应都没有,也无法正确退出程序,一直卡死在一个地方。你只能强制kill。

他项目里面用了一个叫 aiosqlite 的东西(一个在异步环境中读写SQLite数据库的第三方库,可以把数据存到SQLite里,然后通过异步来处理读写操作)。

但是如果说后台线程没能正确结束或等待,就会造成线程阻塞,最终导致Ctrl+C没法正常退出。

于是我便思考了几个思路:

首先,会不会是阻塞了呢?我们都知道…python程序在运行的时候是使用 GIL,这会导致在执行一些I/O或阻塞操作的时候,主程序就会被卡住,无法执行别的事件。

然后还可能是这个数据库操作需要大量读写或者事务提交,把主线程卡死了,动不了了。

于是我开始修改代码,然后在程序退出的时候先关闭它们。

然后结果是逼用没有。那没办法,打印日志来找问题吧。

我自己是直接在python里写了个处理函数,按Ctrl+C后就打出所有线程的堆栈:

import signal
import traceback
import sys
import threading

def print_thread_stacks(signum, frame):
    print("\n*** DEBUGGER SIGNAL RECEIVED ***")
    print("Thread stack traces:\n")
    for thread_id, stack in sys._current_frames().items():
        print(f"# Thread: {threading._active.get(thread_id)}:")
        for filename, lineno, name, line in traceback.extract_stack(stack):
            print(f'  File "{filename}", line {lineno}, in {name}')
            if line:
                print(f"    {line}")
        print()

signal.signal(signal.SIGINT, print_thread_stacks)  # Ctrl+C

这样每次按Ctrl+C就会把所有正在跑的线程堆栈打印出来,看看是不是哪里卡住了。

如果是在Linux上,可以用Python自带的signal库,然后通过kill命令给进程发个对应的信号,同样可以打出堆栈,这样就能知道到底卡在哪里了。

然后我抓到了需要的堆栈日志,一大堆线程都在等:

*** DEBUGGER SIGNAL RECEIVED ***
Thread stack traces:

# Thread: Thread-7:
  File "C:\Users\11635\AppData\Local\Programs\Python\Python312\Lib\threading.py", line 1032, in _bootstrap
    self._bootstrap_inner()
  File "C:\Users\11635\AppData\Local\Programs\Python\Python312\Lib\threading.py", line 1075, in _bootstrap_inner
    self.run()
  File "C:\Users\11635\OneDrive - MSFT\Attachments\Project\Project_Reborn\.venv\Lib\site-packages\aiosqlite\core.py", line 97, in run
    tx_item = self._tx.get()

# Thread: Thread-6:
  File "C:\Users\11635\AppData\Local\Programs\Python\Python312\Lib\threading.py", line 1032, in _bootstrap
    self._bootstrap_inner()
  File "C:\Users\11635\AppData\Local\Programs\Python\Python312\Lib\threading.py", line 1075, in _bootstrap_inner
    self.run()
  File "C:\Users\11635\OneDrive - MSFT\Attachments\Project\Project_Reborn\.venv\Lib\site-packages\aiosqlite\core.py", line 97, in run
    tx_item = self._tx.get()

# ...

这些后台线程一直卡在等待数据库事务的队列上,迟迟无法退出。

然后尝试修复了

但是还是没用,不过这个时候知道问题出在aiosqlite 后台线程上了,于是我开始使用代码来强制退出程序。不至于卡死(总比非要kill强)。

直接在程序退出的时候先显式关闭数据库连接,再使用非常暴力的方法强行终止所有线程,防止它们一直卡住。

def _async_raise(tid, exctype):
    """向线程注入异常"""
    if not isinstance(tid, int):
        tid = tid.ident
    if tid is None:
        return
    res = ctypes.pythonapi.PyThreadState_SetAsyncExc(tid, ctypes.py_object(exctype))
    if res > 1:
        ctypes.pythonapi.PyThreadState_SetAsyncExc(tid, None)

def force_stop_thread(thread):
    """强制停止线程"""
    try:
        _async_raise(thread.ident, SystemExit)
    except Exception:
        pass

def cleanup_threads():
    """清理所有非主线程"""
    main_thread = threading.main_thread()
    current_thread = threading.current_thread()

    # 关闭数据库连接
    try:
        from utils.db import DatabaseManager
        loop = asyncio.new_event_loop()
        asyncio.set_event_loop(loop)
        loop.run_until_complete(DatabaseManager.close_all())
        loop.close()
    except Exception:
        pass

    # 结束所有线程
    for thread in threading.enumerate():
        if thread is not main_thread and thread is not current_thread:
            try:
                if thread.is_alive():
                    force_stop_thread(thread)
            except Exception:
                pass

def force_exit():
    """强制退出程序"""
    try:
        cleanup_thread = threading.Thread(target=cleanup_threads)
        cleanup_thread.daemon = True
        cleanup_thread.start()
        cleanup_thread.join(timeout=0.5)
    finally:
        sys.stderr.flush()
        sys.stdout.flush()
        os._exit(1)

def signal_handler(signum, frame):
    """处理Ctrl+C信号"""
    force_exit()

signal.signal(signal.SIGINT, signal_handler)

这下正常了,至少可以正确退出了。