解决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)
这下正常了,至少可以正确退出了。