事情从一个奇怪的故障说起,一个python写的systemd服务,运行过程中发生故障抛出了异常,本来期望这个不处理的异常能导致进程退出,然后由systemd重新拉起服务修复。实际情况却是,异常抛出了,进程仍然在运行,导致故障没有自愈。
由于服务程序有一个比较复杂的框架,一开始怀疑这个应用程序框架捕获了异常导致进程继续运行。快速搜索了一遍异常处理的代码,发现抛出异常的地方不会再被捕获异常,事情就显得比较诡异了,抛出异常退出的功能运行了很久了。
抓了各线程栈,发现一个线程栈顶是在获取线程锁。读了一下代码,是threading模块在线程退出时等一串锁。代码细节稍微有点复杂,但是栈顶函数的注释很清楚:Wait until the Python thread state of all non-daemon threads get deleted.
就是在等非daemon的线程退出。
排查了所有线程及其子类,发现一处新增的从threading.Timer派生的定时任务,并未设置daemon属性。按照threading模块的说明,子线程线程默认继承daemon属性,API文档中也并未说明其daemon属性有特殊处理,简单验证一下:
也就是Timer线程默认是non-daemon
线程,因此主线程退出是,会等Timer线程退出,即使主线程抛出了异常也会等Timer线程,而Timer线程的执行函数是一个无限循环,不会退出。
简化代码如下:
#! /bin/python3
import sys
import datetime
import threading
import time
def HeartBeat():
print(f"heart-beat start, thread name {threading.current_thread().name}, "
f"id {threading.current_thread().native_id}")
while True:
print(f"[{datetime.datetime.now()}] "
f"{threading.current_thread().name} "
"heart-beat")
time.sleep(1.0)
def main():
main_thread = threading.main_thread()
print(f"main thread name {main_thread.name} id {main_thread.native_id}")
beater = threading.Timer(1.0, function=HeartBeat)
# beater.setDaemon(True)
print(f"[{datetime.datetime.now()}] timer thread {beater.name} start.")
beater.start()
time.sleep(5.0)
print(f"[{datetime.datetime.now()}] main thread exit.")
if __name__ == '__main__':
sys.exit(main())
print("ok")
执行情况如下,可以看到主线程退出后,子线程还一直执行,进程未能退出。
抓取进程栈如下:
将子线程daemon设置为False, 执行情况如下,可见主线程退出后,进程顺利退出。
总结threading线程的daemon机制:
daemon:
A boolean value indicating whether this thread is a daemon thread (True) or not (False). This must be set before start() is called, otherwise RuntimeError is raised. Its initial value is inherited from the creating thread; the main thread is not a daemon thread and therefore all threads created in the main thread default to daemon = False.
The entire Python program exits when no alive non-daemon threads are left.
- 线程的
daemon
属性必须在start()
之前调用,否则会抛出RuntimeError
异常。 - 初始值继承自创建线程。
- 主线程不是daemon线程,因此主线程创建的线程默认都是
non-daemon
线程。 - 只有当所有的
non-daemon
线程都退出后,Python程序(进程)才会退出。