← 返回并发编程目录
← 返回学习笔记首页
专题: Python并发编程系统学习
关键词: Python, 并发编程, threading, Thread, 线程创建, start, join, daemon
一、threading模块简介
Python标准库中的 threading 模块是基于底层 _thread 模块构建的高级线程接口。它提供了比 _thread 更完善、更易用的 API,是日常多线程编程的首选工具。_thread 模块仅提供基础的线程创建和同步原语(如简单的锁),而 threading 模块在此基础上封装了丰富的面向对象接口,让开发者能够以更直观的方式管理线程。
threading 模块围绕几个核心概念展开,下表列出其主要组件及用途:
组件 类/函数 说明
线程对象 Thread 核心类,代表一个独立执行的线程
互斥锁 Lock 最基本的同步原语,确保共享资源的互斥访问
可重入锁 RLock 允许同一线程多次获取的锁,防止死锁
条件变量 Condition 线程间通信机制,基于锁实现等待/通知模式
信号量 Semaphore 限制同时访问资源的线程数量
事件 Event 线程间简单的信号通信,一个线程发信号,其他线程等待
栅栏 Barrier 多个线程相互等待,直到所有线程到达指定点
定时器 Timer 延迟执行线程,继承自 Thread
局部数据 local 线程局部存储,每个线程拥有独立的数据副本
在学习 Thread 类之前,理解线程的基本概念很重要:线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位。一个进程可以拥有多个线程,这些线程共享进程的内存空间和资源,因此线程间通信比进程间通信更为高效,但也带来了数据竞争和同步的挑战。
线程是轻量级的进程。一个进程内的多线程共享全局变量和堆内存,但每个线程拥有独立的栈空间和寄存器上下文。这使得线程创建和切换的开销远小于进程。
二、创建线程的两种方式
threading 模块提供了两种创建线程的典型方式:传递目标函数给 Thread 构造器,或者继承 Thread 类并重写 run() 方法。两种方式各有适用场景,下面分别介绍。
方式一:传递 target 函数
这是最常用、最推荐的方式。将需要在线程中执行的函数作为 target 参数传递给 Thread 构造器,并通过 args 和 kwargs 参数传递函数所需的参数。这种方式代码结构清晰,函数逻辑独立,便于测试和复用。
import threading
import time
def print_numbers (count, delay):
"""线程目标函数:打印数字"""
for i in range(count):
print(f"子线程: {i}" )
time.sleep(delay)
# 创建线程,传递目标函数和参数
t = threading.Thread(
target=print_numbers,
args=(5 , 0.5 ),
name="NumberPrinter"
)
# 启动线程
t.start()
# 等待线程结束
t.join()
print("主线程继续执行" )
target 参数可以是任何可调用对象(callable),包括普通函数、lambda 表达式、类实例(如果类定义了 __call__ 方法)等。args 用于传递位置参数(元组形式),kwargs 用于传递关键字参数(字典形式)。
提示: 即便 target 函数没有参数,也必须显式创建 Thread 对象并调用 start(),线程才会在独立的系统线程中执行。直接调用目标函数只会在当前线程中串行执行。
方式二:继承 Thread 类
当需要在线程中封装更复杂的逻辑,或者需要在线程对象中维护状态时,可以继承 Thread 类并重写其 run() 方法。Thread 类本身实现了 start() 方法启动线程,而 run() 方法定义了线程的主体逻辑。
import threading
import time
class DownloadThread (threading.Thread):
"""自定义线程类:模拟文件下载"""
def __init__ (self , file_id, url):
super ().__init__(name=f"Download-{file_id}" )
self .file_id = file_id
self .url = url
self .progress = 0
def run (self ):
"""线程主逻辑:重写父类的 run 方法"""
print(f"[{self.name}] 开始下载: {self.url}" )
for i in range(10 ):
time.sleep(0.3 )
self .progress = (i + 1 ) * 10
print(f"[{self.name}] 下载进度: {self.progress}%" )
print(f"[{self.name}] 下载完成" )
# 使用自定义线程类
threads = []
for fid in range(1 , 4 ):
t = DownloadThread(fid, f"https://example.com/file{fid}" )
threads.append(t)
t.start()
# 等待所有下载线程完成
for t in threads:
t.join()
print("所有下载任务完成" )
继承 Thread 类的方式特别适合以下场景:需要在线程中维护内部状态(如上例中的 progress)、需要封装多个辅助方法、或者需要在线程执行前后执行固定的初始化和清理逻辑。然而,如果目标逻辑只是一个简单的函数,传递 target 的方式更加轻量和灵活。
核心区别: 传递 target 函数是"组合"方式,将行为委托给外部函数;继承 Thread 类是"继承"方式,将行为封装在线程类内部。Python 推崇组合优于继承,因此若无特殊需求,优先使用 target 方式。
三、start() 与 run() 的区别
理解 start() 和 run() 的区别是掌握多线程的关键之一。很多初学者容易混淆这两个方法,下面用对比方式说明。
start() 方法
start() 是启动线程的唯一正确方式。调用 start() 时,threading 模块会在底层创建一个真正的操作系统线程,然后在这个新线程中自动调用 run() 方法。因此,start() 立即返回(不阻塞),新线程和调用线程并发执行。一个 Thread 实例只能调用一次 start(),重复调用会抛出 RuntimeError。
run() 方法
run() 方法定义了线程的执行体。如果直接调用 run(),它只会在当前线程中作为一个普通方法执行,不会创建新的系统线程。这相当于普通的方法调用,完全失去了多线程的并发效果。因此,永远不要直接调用 run(),而是通过 start() 间接调用。
import threading
import time
def worker (name):
print(f"{name} 运行在线程: {threading.current_thread().name}" )
time.sleep(1 )
t = threading.Thread(target=worker, args=("线程A" ,))
# 情况一:直接调用 run() - 不会创建新线程
t.run() # 输出: 线程A 运行在线程: MainThread
# 情况二:调用 start() - 创建新的系统线程
t = threading.Thread(target=worker, args=("线程B" ,))
t.start() # 输出: 线程B 运行在线程: Thread-1 (或其他非 MainThread 名称)
t.join()
重要: 同一个 Thread 对象不能两次调用 start()。如果确实需要再次执行相同的任务,必须创建新的 Thread 实例。
四、join() 等待线程结束
主线程启动子线程后,默认情况下主线程不会等待子线程结束——主线程会继续执行自己的后续代码,甚至可能在子线程结束前就退出程序。使用 join() 方法可以阻塞调用线程,直到目标线程执行完毕。
join() 的基本用法
import threading
import time
def slow_worker ():
print("子线程开始工作..." )
time.sleep(3 )
print("子线程工作结束" )
t = threading.Thread(target=slow_worker)
t.start()
# 主线程阻塞,等待子线程结束
print("主线程等待子线程..." )
t.join()
print("主线程继续执行" )
timeout 参数
join() 接受一个可选的 timeout 参数(单位为秒)。设置了 timeout 后,调用线程最多等待指定的秒数,如果目标线程仍未结束,调用线程会继续执行。timeout 参数在处理可能无限阻塞的线程时非常有用。
import threading
import time
def long_task ():
time.sleep(10 )
t = threading.Thread(target=long_task, daemon=True )
t.start()
# 最多等待 2 秒
t.join(timeout=2 )
if t.is_alive():
print("子线程尚未结束,主线程不再等待" )
else :
print("子线程已结束" )
需要注意,join(timeout) 返回后,即使超时,目标线程仍会在后台继续运行(除非它被设为守护线程)。timeout 参数只控制调用方的等待时长,并不能强制终止目标线程。
最佳实践: 在程序退出前,始终对非守护线程调用 join(),以确保它们能够正常完成。对于可能无法正常结束的线程,设置合理的 timeout 并在超时后记录日志或执行清理逻辑。
五、daemon 守护线程
守护线程(daemon thread)是一种特殊的线程,它的生命周期依赖于创建它的线程。当所有非守护线程(包括主线程)结束时,Python 解释器会强制终止所有剩余的守护线程并退出程序。
daemon 标志的设置
daemon 标志可以在创建 Thread 时通过 daemon 参数设置,也可以在启动线程之前通过 setDaemon() 方法(Python 3.10+ 推荐直接设置属性)设置。一旦线程启动,daemon 标志就不能再修改。
import threading
import time
def daemon_worker ():
while True :
print("守护线程运行中..." )
time.sleep(1 )
# 创建守护线程
d = threading.Thread(target=daemon_worker, daemon=True )
d.start()
# 主线程 3 秒后结束,守护线程被强制终止
time.sleep(3 )
print("主线程结束,守护线程将被终止" )
运行上述代码,主线程会在 3 秒后退出,程序随之结束,守护线程被强制终止。如果将 daemon=True 改为 daemon=False(默认值),则主线程结束后程序不会退出,会一直等待守护线程的无限循环。
守护线程的典型应用场景
守护线程适合执行那些不应该阻止程序退出的后台任务,典型场景包括:
心跳检测: 定期向监控系统发送心跳信号,程序退出时心跳自然停止
日志刷新: 在后台定时将日志缓冲区写入磁盘
缓存清理: 定时扫描并清理过期缓存条目
后台预热: 应用启动后预加载数据或预热连接池
垃圾回收: 在后台执行非关键的资源回收工作
注意: 守护线程被强制终止时不会执行清理代码(如 finally 块和 with 语句的退出),因此不适合操作关键资源(如打开的文件、数据库连接等)。如果线程必须确保资源被正确释放,应使用非守护线程并设计优雅的退出机制。
六、线程属性:name / ident / is_alive / native_id
Thread 对象提供了多个有用的属性,用于标识和查询线程的状态。理解这些属性有助于在调试和日志记录中准确识别线程。
name 线程名称
每个线程都可以有一个人类可读的名称。如果在创建时不指定,Python 会自动生成一个格式为 Thread-1、Thread-2 的默认名称。建议为重要线程指定有意义的名称,便于日志分析和问题定位。
import threading
t1 = threading.Thread(name="Worker-HTTP" )
t2 = threading.Thread() # 自动命名为 Thread-1
print(t1.name) # Worker-HTTP
print(t2.name) # Thread-1
# 名称可以重复,也可在运行时修改
t1.name = "Worker-Main"
ident 线程标识符
ident 是一个非零整数,在调用 start() 后才被赋值。它是操作系统层面的线程 ID(在特定平台上),在 Python 解释器生命周期内唯一标识一个线程。如果线程尚未启动或已结束,ident 为 None。注意,线程结束后其 ident 可能被操作系统回收并分配给新创建的线程。
t = threading.Thread(target=lambda : None )
print(t.ident) # None,线程尚未启动
t.start()
print(t.ident) # 例如: 12345,线程已启动
native_id 原生线程 ID
Python 3.8 中引入的 native_id 属性直接对应操作系统的线程 ID(在 Windows 上为 TID,在 Linux 上为轻量级进程的 PID)。这个 ID 由操作系统分配,在整个系统范围内唯一,可用于与系统监控工具(如 top、htop、Process Explorer)报告的信息进行关联。
t = threading.Thread(target=lambda : None )
t.start()
print(t.native_id) # 例如在 Linux 上: 14024
is_alive() 线程存活状态
is_alive() 方法返回一个布尔值,表示线程是否仍在执行。在线程调用 start() 之后、run() 方法结束之前,is_alive() 返回 True;其他情况返回 False。这个方法常用于在 join() 超时后检查线程状态。
import time
def quick_task ():
time.sleep(2 )
t = threading.Thread(target=quick_task)
print(t.is_alive()) # False,未启动
t.start()
print(t.is_alive()) # True,已启动且正在运行
t.join()
print(t.is_alive()) # False,执行完毕
属性 类型 含义 修改时机
name str 线程的人类可读名称,默认格式 Thread-N 创建时或 start() 前
ident int 或 None 线程的 Python 内部标识符,start() 后有效 只读,启动后自动设置
native_id int 或 None 操作系统级别的线程 ID(Python 3.8+) 只读,启动后自动设置
daemon bool 是否守护线程 创建时或 start() 前
is_alive() bool(方法) 线程是否正在执行 运行时动态查询
七、threading 模块的辅助函数
除了 Thread 类,threading 模块还提供了一系列实用的辅助函数,帮助开发者获取当前线程环境的信息并管理线程集合。
threading.active_count()
返回当前存活(alive)的线程数量,不包括尚未启动或已经结束的线程。这个计数包括主线程和所有已启动的非守护线程及守护线程。在调试线程泄漏问题时非常有用。
import threading
import time
def spawn_threads ():
for i in range(5 ):
t = threading.Thread(target=time.sleep, args=(2 ,))
t.start()
print(f"启动前活跃线程数: {threading.active_count()}" ) # 至少为 1(主线程)
spawn_threads()
print(f"启动后活跃线程数: {threading.active_count()}" ) # 1 + 5 = 6
threading.current_thread()
返回当前调用线程的 Thread 对象,是获取当前线程信息的入口。常用于判断当前代码运行在哪个线程上下文中,以进行定制化的日志输出。
import threading
def show_current ():
t = threading.current_thread()
print(f"当前线程: {t.name}, ident: {t.ident}" )
# 在主线程中调用
show_current()
# 在子线程中调用
t = threading.Thread(target=show_current, name="ChildThread" )
t.start()
t.join()
threading.enumerate()
返回当前所有存活的 Thread 对象列表。这个列表包括主线程、所有已启动但尚未结束的非守护线程和守护线程。需要注意的是,enumerate() 返回的是快照,遍历过程中线程状态可能发生变化。
import threading
def list_threads ():
threads = threading.enumerate()
print(f"当前存活线程数: {len(threads)}" )
for t in threads:
print(f" - {t.name} (daemon={t.daemon}, alive={t.is_alive()})" )
list_threads()
threading.main_thread()
Python 3.4 中引入,返回主线程的 Thread 对象。主线程是程序启动时自动创建的线程,它是所有其他线程的父线程。当主线程结束时,程序通常会退出(除非有非守护子线程仍在运行)。
import threading
main = threading.main_thread()
print(f"主线程: {main.name}" ) # MainThread
print(f"主线程 ident: {main.ident}" )
下表总结了这些辅助函数的用途和使用建议:
函数 返回值 主要用途
active_count() int 监控线程数量、检测线程泄漏
current_thread() Thread 对象 获取当前执行上下文、定制日志
enumerate() Thread 对象列表 遍历所有存活线程、统一管理
main_thread() Thread 对象 与主线程比较、处理主线程特有逻辑
八、常见用法与最佳实践
掌握了 Thread 类的基础用法后,还需要在实际开发中遵循一些最佳实践,以确保多线程程序的正确性、可维护性和性能。
线程数量的控制
并非线程越多越好。线程的创建和切换都有开销,过多的线程会导致上下文切换频繁,反而降低系统吞吐量。合理的线程数量取决于任务的类型:
CPU 密集型任务: 线程数量通常设置为 CPU 核心数 + 1 或核心数的 1~2 倍。由于 CPython 的 GIL(全局解释器锁),纯 CPU 密集型任务在多线程下无法利用多核并行,此时应优先考虑多进程(multiprocessing 模块)。
I/O 密集型任务: 可以创建较多的线程,因为大部分时间线程都在等待 I/O 操作完成(网络请求、文件读写、数据库查询等)。线程数量通常根据 I/O 延迟和期望的并发量来估算。
混合型任务: 可以设置线程池,根据实际业务场景动态调整线程数。一般推荐使用 concurrent.futures.ThreadPoolExecutor 而非手动管理线程。
经验法则: 对于 I/O 密集型应用,线程数可以设置为 "期望的并发数" 或 "2 * CPU 核心数 + 2" 作为起点,然后通过性能测试调优。对于 CPU 密集型应用,在 CPython 下应使用多进程而非多线程。
线程命名规范
为线程设定有意义的名称是良好的工程实践。在开发和调试阶段,清晰的线程名可以极大地提升日志的可读性和问题定位的效率。
import threading
def worker (task_id):
t = threading.current_thread()
print(f"[{t.name}] 开始处理任务 {task_id}" )
for i in range(3 ):
t = threading.Thread(
target=worker,
args=(i,),
name=f"WorkerPool-{i}"
)
t.start()
命名建议:使用功能前缀加唯一标识符的格式,例如 HTTPListener-1、DBWriter-Main、CacheCleaner-Daemon。避免使用无意义的默认名称。
异常处理策略
线程内发生的异常不会自动传播到主线程。如果主线程需要感知子线程中发生的异常,需要自行设计异常传递机制。以下是一种常见的做法:
import threading
import sys
class ExceptionAwareThread (threading.Thread):
def __init__ (self , target=None , args=()):
super ().__init__()
self ._target = target
self ._args = args
self .exception = None
def run (self ):
try :
if self ._target:
self ._target(*self ._args)
except BaseException as e:
self .exception = e
raise
# 使用示例
def risky_task ():
raise ValueError("发生异常" )
t = ExceptionAwareThread(target=risky_task)
t.start()
t.join()
if t.exception:
print(f"捕获到线程异常: {t.exception}" )
其他实践要点
优先使用线程池: 对于需要频繁创建和销毁线程的场景,使用 concurrent.futures.ThreadPoolExecutor 或 multiprocessing.pool.ThreadPool 可以复用线程,减少创建开销。
避免共享状态: 线程间尽量少共享可变数据。如果必须共享,使用 threading.Lock 或其他同步原语保护临界区。
使用线程局部数据: 每个线程独立的数据可以使用 threading.local() 存储,无需加锁。
注意 GIL 的影响: CPython 的 GIL 限制了多线程对 CPU 密集型任务的并行加速,这时应考虑使用多进程或异步编程。
优雅关闭: 为线程设计退出信号(如使用 threading.Event),避免使用强制终止手段。
日志中添加线程信息: 在日志格式中加入 %(threadName)s,方便区分不同线程的输出。
# logging 配置中加入线程名
import logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s [%(threadName)s] %(levelname)s: %(message)s' ,
datefmt='%H:%M:%S'
)
def log_worker ():
logging.info("线程开始工作" )
t = threading.Thread(target=log_worker, name="MyWorker" )
t.start()
t.join()
# 输出示例: 14:30:22 [MyWorker] INFO: 线程开始工作
总结: Thread 类是 Python 多线程编程的基石。掌握其创建方式、生命周期管理(start/join)、守护线程机制以及相关属性,是构建可靠多线程程序的第一步。在实际项目中,结合线程池、同步原语和合理的线程命名规范,可以写出既高效又易于维护的并发代码。下一篇文章将深入探讨多线程同步机制——锁(Lock)与线程安全。