threading模块:Thread类与多线程创建

Python并发编程专题 · 掌握多线程编程的入口

专题: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 构造器,并通过 argskwargs 参数传递函数所需的参数。这种方式代码结构清晰,函数逻辑独立,便于测试和复用。

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-1Thread-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 解释器生命周期内唯一标识一个线程。如果线程尚未启动或已结束,identNone。注意,线程结束后其 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,执行完毕
属性类型含义修改时机
namestr线程的人类可读名称,默认格式 Thread-N创建时或 start() 前
identint 或 None线程的 Python 内部标识符,start() 后有效只读,启动后自动设置
native_idint 或 None操作系统级别的线程 ID(Python 3.8+)只读,启动后自动设置
daemonbool是否守护线程创建时或 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 类的基础用法后,还需要在实际开发中遵循一些最佳实践,以确保多线程程序的正确性、可维护性和性能。

线程数量的控制

并非线程越多越好。线程的创建和切换都有开销,过多的线程会导致上下文切换频繁,反而降低系统吞吐量。合理的线程数量取决于任务的类型:

经验法则:对于 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-1DBWriter-MainCacheCleaner-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}")

其他实践要点

# 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)与线程安全。