← 返回并发编程目录
← 返回学习笔记首页
专题: Python并发编程系统学习
关键词: Python, 并发编程, 守护线程, daemon, 线程生命周期, 线程退出, 资源清理
一、线程生命周期五阶段
在Python中,线程并非创建后立即运行,也不是无限期存活。每一个线程都经历从创建到销毁的完整生命周期,理解这五个阶段是掌握多线程编程的基础。线程生命周期的五个阶段分别是:新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Dead)。这五个阶段构成了一个完整的状态机模型,贯穿线程的整个生命过程。
┌──────────┐ start() ┌───────────┐ 调度器选中 ┌───────────┐
│ New │ ──────────────→ │ Runnable │ ──────────────→ │ Running │
│ (新建) │ │ (就绪) │ │ (运行) │
└──────────┘ └───────────┘ └─────┬─────┘
↑ │
│ ┌───────────┐ │
│ │ Dead │ ←───── run() ────────┤
│ │ (死亡) │ 返回 │
│ └───────────┘ │
│ │
│ 等待I/O、sleep、
│ 获取锁、join
│ │
│ ┌───────────┐ │
└─────────────────── │ Blocked │ ←─────────────────────┘
新线程实例创建 │ (阻塞) │
└───────────┘
│ ↑
│ │
└────┘
条件满足,回到就绪
新建(New)阶段: 当使用 threading.Thread(target=func) 创建一个线程实例时,线程处于"新建"状态。此时线程对象已经存在,但尚未与操作系统级线程关联,也未分配任何系统资源。在这个阶段,我们可以对线程进行配置,例如设置名称、设置daemon标志、传递参数等。这是线程生命周期的起点。
就绪(Runnable)阶段: 调用 thread.start() 之后,线程进入"就绪"状态。此时底层操作系统线程已被创建,获得了独立的执行栈空间,正在等待CPU调度器将其选中执行。在就绪队列中,可能有多个线程同时等待,调度器根据调度策略(时间片轮转、优先级等)决定下一个运行的线程。值得注意的是,start() 返回后,线程不一定立即开始执行,它可能还在就绪队列中排队。
运行(Running)阶段: 当CPU调度器选中了该线程,线程就开始执行 run() 方法中的代码,此时处于"运行"状态。在单核CPU上,同一时刻只有一个线程在运行,多线程通过时间片快速切换实现"并发"效果。运行中的线程可能会因为以下原因离开运行状态:时间片耗尽自动让出CPU、主动执行阻塞操作、或者任务完成正常终止。
阻塞(Blocked)阶段: 当线程执行阻塞操作时,例如调用 time.sleep()、等待I/O操作完成、尝试获取已被占用的锁、或者调用 thread.join() 等待其他线程,线程就会进入"阻塞"状态。阻塞状态的线程不参与CPU调度,直到阻塞条件解除(如sleep时间到、I/O完成、锁被释放),才会重新回到"就绪"队列等待调度。阻塞状态是提高CPU利用率的关键机制,让CPU在等待期间可以去执行其他线程。
死亡(Dead)阶段: 当线程的 run() 方法正常返回,或者 run() 方法中抛出未捕获的异常时,线程进入"死亡"状态。死亡状态的线程将被操作系统回收资源,无法重新启动——一个线程只能被 start() 恰好一次。试图对已死亡的线程调用 start() 会抛出 RuntimeError。在Python中,可以通过 thread.is_alive() 方法判断线程是否仍在存活。
核心要点: 线程状态转换中的关键方法是 start() 和 run()。混淆这两个方法是初学者最常见的错误。直接调用 run() 不会启动新线程,它只是在当前线程中同步执行目标函数。只有 start() 才会创建新线程并自动调用 run()。
import threading
import time
def worker ():
print("子线程运行中" )
time.sleep(2 )
print("子线程即将结束" )
t = threading.Thread(target=worker)
print("线程状态:" , t.is_alive()) # False — 新建状态
t.start()
print("线程状态:" , t.is_alive()) # True — 运行或就绪
t.join()
print("线程状态:" , t.is_alive()) # False — 死亡
二、daemon标志详解
守护线程(daemon thread)是Python线程模型中一个非常重要的概念。daemon标志决定了线程的"守护性质":当主线程(或者说,所有非守护线程)退出时,所有剩余的守护线程会被强制终止。换句话说,守护线程的生命周期依赖于创建它的进程和其他非守护线程的存在。
2.1 daemon=True 与 daemon=False 的行为区别
daemon=False(默认值): 这是线程的默认行为。非守护线程会阻止程序退出。只要还有一个非守护线程存活,主线程就不会退出,即使主线程的代码已经执行完毕。程序会等待所有非守护线程都结束后才真正退出。这确保了重要的后台工作不会被突然中断。
daemon=True: 守护线程不会阻止程序退出。当所有非守护线程都结束时,Python解释器会在退出前强制终止所有守护线程,无论这些守护线程正在执行什么操作。守护线程通常用于执行那些不需要持久化状态的后台任务,例如监控心跳、定时刷新缓存、日志轮转等。守护线程的突然终止意味着它没有机会执行清理操作。
重要警告: 守护线程在终止时不会执行任何清理代码(如finally块、上下文管理器的__exit__方法),因为它的终止方式是强制性的。因此,守护线程不适合操作共享资源(如文件写入、数据库事务等)。如果需要执行清理操作,应该使用非守护线程配合信号机制。
2.2 daemon标志的设置时机
daemon标志必须在调用 start() 方法之前设置。在 start() 之后设置daemon会抛出 RuntimeError。这是因为一旦线程启动,其性质已经确定,Python运行时不允许运行时改变它的守护状态。可以通过构造函数参数或属性赋值两种方式设置:
import threading
import time
def daemon_worker ():
while True :
print("守护线程工作中..." )
time.sleep(1 )
def normal_worker ():
print("普通线程启动" )
time.sleep(3 )
print("普通线程结束" )
d = threading.Thread(target=daemon_worker, daemon=True )
n = threading.Thread(target=normal_worker)
d.start()
n.start()
# 主线程等待3秒后退出,守护线程被强制终止
在上面的示例中,守护线程每1秒打印一次消息,而普通线程运行3秒后结束。当普通线程结束后,主线程随之退出,此时守护线程会被强制终止——它不会打印任何终止信息,也不会执行任何清理代码。程序的总运行时间约为3秒,而不是无限期运行。
2.3 守护线程的实际应用场景
虽然守护线程存在资源安全问题,但在很多场景下仍然非常有用:
心跳监控: 定期向监控系统发送"我还活着"的信号
缓存刷新: 定期从远程加载最新配置或数据到内存
日志轮转: 在后台检查日志文件大小并在达到阈值时轮转
连接池维护: 定期检查连接池中的连接是否健康,清理失效连接
统计采集: 定期采集系统指标(CPU、内存、QPS等)供监控面板使用
三、线程的join超时控制
join() 方法是线程协调的核心工具。调用 thread.join() 会使当前线程阻塞,直到目标线程终止。这在需要等待其他线程完成时非常有用,但不当使用也可能导致程序永久挂起。
3.1 join(timeout)参数详解
join() 接受一个可选的 timeout 参数(以秒为单位的浮点数)。当指定了timeout时,join() 最多阻塞timeout秒,之后无论目标线程是否结束都会返回。这在防止死锁和提供响应性方面至关重要:
import threading
import time
def slow_worker ():
time.sleep(10 )
print("慢线程结束" )
t = threading.Thread(target=slow_worker)
t.start()
# 最多等2秒,不等完
t.join(timeout=2.0 )
if t.is_alive():
print("线程仍在运行,继续执行其他操作" )
# 这里可以检查线程状态并做备选处理
3.2 超时后的线程状态处理
当 join(timeout) 超时返回时,目标线程可能仍然存活。此时需要程序主动决定如何处理:是继续等待、放弃等待并继续执行,还是采取某种措施终止线程(注意Python线程本身不支持强制终止,需要通过信号机制通知线程自行退出)。一个常见的模式是使用循环多次join,或者记录超时信息供后续监控使用:
def wait_with_timeout (thread, timeout, step=0.5 ):
"""带超时的等待,提供进度反馈"""
elapsed = 0.0
while elapsed < timeout and thread.is_alive():
thread.join(timeout=min(step, timeout - elapsed))
elapsed += step
if thread.is_alive():
print(f"等待中...已等待 {elapsed:.1f} 秒" )
return not thread.is_alive() # True表示线程已完成
四、线程安全退出的策略
与一些语言不同,Python不提供强制终止线程的API(如Java的 Thread.stop() 已被废弃)。正确的做法是通过协作式信号机制通知线程自行退出。这需要线程在代码中主动检查退出标志。
4.1 使用Event信号通知线程退出
threading.Event 是Python提供的最常用的线程间信号机制。它内部维护一个bool标志,线程可以等待标志被设置,也可以通过 wait() 方法阻塞直到标志被设置。Event是线程安全的,适合作为退出通知信号:
import threading
import time
stop_event = threading.Event()
def graceful_worker ():
while not stop_event.is_set():
# 工作循环
stop_event.wait(1 ) # 超时1秒,避免忙等待
if not stop_event.is_set():
print("执行工作单元..." )
print("收到退出信号,清理资源..." )
# 执行清理操作
t = threading.Thread(target=graceful_worker)
t.start()
time.sleep(5 )
print("发送停止信号" )
stop_event.set()
t.join()
print("线程已安全退出" )
Event.wait(timeout) 的设计非常巧妙:它会在timeout超时或者标志被设置时返回。结合返回值(True表示标志被设置,False表示超时),我们可以既保持响应的及时性,又避免CPU空转(忙等待)。
4.2 使用自定义stop标志
除了Event,也可以使用简单的原子变量作为停止标志。Python中的普通bool变量在多线程环境下也具有一定的安全性,但使用 threading.Event 或 threading.Lock 保护更为可靠。更进阶的做法是使用 threading.Condition 实现更复杂的通知逻辑:
import threading
class StoppableThread (threading.Thread):
"""可安全停止的工作线程封装"""
def __init__ (self , *args, **kwargs):
super ().__init__(*args, **kwargs)
self ._stop_event = threading.Event()
def stop (self ):
"""请求线程停止"""
self ._stop_event.set()
def stopped (self ):
"""检查是否收到停止信号"""
return self ._stop_event.is_set()
def run (self ):
try :
while not self .stopped():
# 子类在此实现工作逻辑
pass
finally :
self ._cleanup()
def _cleanup (self ):
"""释放资源,子类可覆盖"""
print(f"线程 {self.name} 资源清理完毕" )
4.3 try/finally确保资源清理
无论线程是正常结束还是因为异常而终止,资源清理代码都应该被可靠执行。try/finally 是实现这一目标的根本手段。在使用 with 语句(上下文管理器)时,实际上也是基于 try/finally 的语法糖。对于需要跨线程访问的资源,建议使用 threading.Lock、threading.RLock 或 threading.Semaphore 进行同步保护:
def safe_worker (filepath, stop_event):
"""安全的文件写入线程"""
file_handle = None
try :
file_handle = open(filepath, 'a' )
while not stop_event.is_set():
stop_event.wait(1 )
if not stop_event.is_set():
file_handle.write("心跳数据\n" )
file_handle.flush()
finally :
if file_handle is not None :
file_handle.close()
print("文件已安全关闭" )
五、线程异常与未捕获异常处理
在线程中发生的异常处理与主线程有所不同。由于每个线程都有自己的执行栈,子线程中抛出的未捕获异常不会自动传播到主线程。如果不加以处理,这些异常会导致子线程立刻终止,而主线程可能对此毫不知情。
5.1 threading.excepthook 机制(Python 3.8+)
从Python 3.8开始,threading 模块引入了 excepthook 机制。当一个线程抛出未捕获的异常时,Python会调用 threading.excepthook 函数。我们可以通过设置自定义的excepthook来集中处理所有线程中的未捕获异常:
import threading
import sys
def custom_excepthook (args):
"""自定义线程异常处理钩子"""
print(f"[ERROR] 线程 [{args.name}] 发生异常:" )
print(f" 异常类型: {args.exc_type.__name__}" )
print(f" 异常信息: {args.exc_value}" )
# 可以在这里记录日志或发送告警
# 替换默认的线程异常钩子
threading.excepthook = custom_excepthook
def buggy_worker ():
1 / 0 # 故意制造除零错误
t = threading.Thread(target=buggy_worker, name="BuggyThread" )
t.start()
t.join()
print("主线程仍然继续运行" )
需要注意的是,threading.excepthook 只在线程因未捕获异常而终止时被调用。如果异常在 run() 方法内部被 try/except 捕获并处理了,则不会触发这个钩子。
5.2 在run方法中集中处理异常
另一个推荐的做法是在线程的 run() 方法中使用包装器模式,集中处理所有异常。这样既能保证异常不会导致线程静默消失,又能执行统一的清理操作:
class SafeThread (threading.Thread):
"""带异常安全的线程基类"""
def run (self ):
try :
if self ._target:
self ._target(*self ._args, **self ._kwargs)
except Exception as e:
print(f"线程 [{self.name}] 发生异常: {e}" )
# 记录日志、发送告警等
finally :
self ._cleanup()
def _cleanup (self ):
# 释放线程持有的资源
pass
最佳实践: 永远不要让线程静默消失。每个线程都应该有异常处理机制,要么在 run() 内使用 try/except 捕获,要么设置全局的 threading.excepthook。未捕获的异常等于潜在的bug——它不仅可能导致数据损坏,还让你在调试时无从下手。
六、最佳实践
综合以上知识,这里总结一组经过实践检验的线程生命周期管理最佳实践:
6.1 尽量少用守护线程
守护线程的核心问题是无法安全退出。由于守护线程在程序退出时被强制终止,它没有机会执行任何清理操作——文件不会被刷新关闭、网络连接不会被优雅关闭、数据库事务不会被提交或回滚。在可能的情况下,应该优先使用非守护线程配合 join() 等待其自然结束。如果必须使用守护线程,确保它不操作任何持久化资源。
6.2 明确资源清理策略
每个线程都应该有明确的资源清理策略。对于非守护线程,在 run() 方法中使用 try/finally 确保资源释放。对于涉及文件、网络连接、数据库会话等需要显式关闭的资源,使用 with 语句自动管理。对于线程池中的工作线程,重写 run() 方法并在其中添加清理逻辑:
def thread_cleanup_example ():
"""线程资源清理的标准模式"""
with open("output.log" , "a" ) as f:
with threading.Lock(): # 保护共享资源
f.write("线程安全写入\n" )
# with语句自动确保了文件关闭
6.3 主线程做好join管理
主线程应当对所有非守护子线程调用 join(),确保它们有机会正常结束。在实际应用中,经常会出现主线程已经结束而某些子线程仍在运行的情况,导致程序行为不符合预期。一个健壮的模式是:
在程序启动时创建所有工作线程
对每个工作线程设置适当的退出信号机制(Event)
在程序退出前的清理阶段设置停止信号
对所有非守护线程调用 join(timeout)
对超时的线程记录警告日志
import threading
import time
import signal
class ThreadManager :
"""线程管理器:统一管理线程的创建和退出"""
def __init__ (self ):
self .threads = []
self .stop_event = threading.Event()
def add_thread (self , target, daemon=False ):
"""添加一个受管理的工作线程"""
t = threading.Thread(
target=target,
args=(self .stop_event,),
daemon=daemon
)
self .threads.append(t)
return t
def start_all (self ):
"""启动所有线程"""
for t in self .threads:
t.start()
def shutdown (self , timeout=5.0 ):
"""安全关闭所有线程"""
print("发送停止信号..." )
self .stop_event.set()
for t in self .threads:
if t.is_alive() and not t.daemon:
t.join(timeout=timeout)
if t.is_alive():
print(f"警告: 线程 [{t.name}] 未能在 {timeout} 秒内退出" )
print("所有线程关闭完成" )
6.4 避免使用 threading.Timer 作为守护线程
threading.Timer 创建的定时线程默认是守护线程。如果在定时器触发之前程序就退出了,定时任务不会执行。如果需要在程序退出前确保定时任务得到执行,建议手动创建非守护线程并自行实现定时逻辑。
6.5 善用线程池替代手动管理
对于大多数业务场景,concurrent.futures.ThreadPoolExecutor 是比手动管理线程更好的选择。线程池自动处理线程的创建、复用和销毁,并且提供了 shutdown(wait=True) 方法确保所有提交的任务在退出前完成。线程池使用非守护线程执行任务,并提供了统一的异常处理和结果获取机制。
from concurrent.futures import ThreadPoolExecutor
import time
def task (n):
time.sleep(n)
return f"任务 {n} 完成"
with ThreadPoolExecutor(max_workers=3 ) as executor:
futures = [executor.submit(task, i) for i in [1 , 2 , 3 ]]
for f in futures:
print(f.result())
# with语句退出时自动调用shutdown(wait=True)
总结: 守护线程与线程生命周期管理是Python并发编程的基础。核心要点是:(1) 线程有5个明确的生命周期阶段,理解状态转换是基础;(2) daemon标志必须在start()之前设置,守护线程不可靠;(3) 永远不要依赖守护线程执行关键操作;(4) 使用Event等信号机制实现线程的优雅退出;(5) 始终使用try/finally确保资源清理;(6) 在可行的前提下,优先使用线程池而非手动管理。