Python3 threading 模块
Python 的
原因:
threading模块是实现多线程编程的核心工具,相比进程,线程更轻量、创建开销更低,且共享进程内存空间,适合 I/O 密集型任务(如网络请求、文件读写)的并发处理。本文从基础用法、线程同步、实战场景到核心坑点,全面拆解threading模块的使用逻辑,结合可运行示例,帮你掌握多线程编程的核心技巧。
一、核心认知:线程与 threading 模块的定位
1. 线程的特点(对比多进程)
- 轻量级:线程创建 / 销毁的开销远低于进程,占用内存更少;
- 共享内存:同一进程内的线程共享全局变量、文件句柄等资源,无需额外通信机制;
- GIL 限制:Python 的全局解释器锁(GIL)导致同一时刻仅一个线程执行 Python 字节码,CPU 密集型任务无法通过多线程实现真正并行(需用
multiprocessing),但 I/O 密集型任务仍能通过线程切换提升效率。
2. threading 模块的核心组件
| 组件 | 作用 |
|---|---|
Thread |
线程类,用于创建和管理线程 |
Lock/RLock |
锁机制,解决资源竞争问题 |
Semaphore |
信号量,控制并发线程数 |
Event |
线程间事件通知 |
Condition |
条件变量,实现复杂同步 |
Timer |
定时线程,延迟执行任务 |
二、基础用法:创建与管理线程
threading模块创建线程有两种核心方式:传入目标函数(简单场景)、继承 Thread 类(复杂场景)。1. 方式 1:传入 target 函数(最常用)
适用于逻辑简单的任务,直接将待执行的函数传入
Thread对象,通过start()启动线程,join()等待线程结束。示例:基础线程创建与执行
import threading
import time
# 定义线程执行的函数
def task(name, delay):
print(f"线程{name}开始执行,延迟{delay}秒")
time.sleep(delay) # 模拟I/O操作(如网络请求)
print(f"线程{name}执行完成")
if __name__ == "__main__":
# 创建线程(daemon=False表示非守护线程,默认)
t1 = threading.Thread(target=task, args=("A", 2))
t2 = threading.Thread(target=task, args=("B", 1))
# 启动线程(此时线程进入就绪状态,由系统调度执行)
t1.start()
t2.start()
# 等待线程执行完成(主线程阻塞,直到t1、t2结束)
print("主线程等待子线程完成...")
t1.join()
t2.join()
print("所有线程执行完毕,主线程结束")
输出结果(线程执行顺序由系统调度,t2 可能先完成):
线程A开始执行,延迟2秒
线程B开始执行,延迟1秒
主线程等待子线程完成...
线程B执行完成
线程A执行完成
所有线程执行完毕,主线程结束
2. 方式 2:继承 Thread 类(自定义线程)
适用于需要扩展线程功能的场景(如重写
__init__初始化参数、自定义run()方法)。示例:自定义线程类
import threading
import time
class MyThread(threading.Thread):
def __init__(self, name, delay):
super().__init__() # 必须调用父类构造方法
self.name = name
self.delay = delay
# 重写run()方法,线程启动后自动执行
def run(self):
print(f"自定义线程{self.name}启动,延迟{self.delay}秒")
time.sleep(self.delay)
print(f"自定义线程{self.name}结束")
if __name__ == "__main__":
t1 = MyThread("C", 3)
t2 = MyThread("D", 1)
t1.start()
t2.start()
t1.join()
t2.join()
print("自定义线程全部执行完毕")
3. 关键方法与属性
| 方法 / 属性 | 作用 |
|---|---|
start() |
启动线程(仅能调用一次,重复调用会报错) |
join(timeout) |
等待线程结束,timeout 为可选超时时间(秒) |
is_alive() |
判断线程是否处于活动状态(启动后、结束前为 True) |
daemon |
设置是否为守护线程(True 表示守护线程,主线程结束则子线程强制终止) |
name |
线程名称(可自定义,默认 Thread-1、Thread-2...) |
守护线程示例(主线程结束,子线程强制终止):
import threading
import time
def daemon_task():
while True:
print("守护线程运行中...")
time.sleep(1)
if __name__ == "__main__":
# 设置daemon=True,成为守护线程
t = threading.Thread(target=daemon_task, daemon=True)
t.start()
time.sleep(3) # 主线程休眠3秒
print("主线程结束,守护线程被强制终止")
输出:
守护线程运行中...
守护线程运行中...
守护线程运行中...
主线程结束,守护线程被强制终止
三、线程同步:解决资源竞争问题
多线程共享内存时,若同时修改同一资源(如全局变量),会引发竞态条件(Race Condition),导致数据错误。
threading模块提供多种同步工具,核心是Lock(锁)。1. 问题重现:未加锁的资源竞争
import threading
# 共享资源
count = 0
def increment():
global count
for _ in range(100000):
count += 1 # 非原子操作(读取→加1→写入)
if __name__ == "__main__":
t1 = threading.Thread(target=increment)
t2 = threading.Thread(target=increment)
t1.start()
t2.start()
t1.join()
t2.join()
print(f"最终count值:{count}") # 预期200000,实际远小于该值
原因:count += 1不是原子操作,两个线程同时读取、修改 count,导致数据覆盖。
2. 解决方案 1:Lock(互斥锁)
Lock是最基础的同步工具,通过acquire()获取锁,release()释放锁,确保同一时刻仅一个线程操作共享资源。加锁后的示例:
import threading
count = 0
lock = threading.Lock() # 创建锁对象
def increment():
global count
for _ in range(100000):
lock.acquire() # 获取锁(若锁被占用则阻塞)
try:
count += 1 # 临界区:仅一个线程能执行
finally:
lock.release() # 确保锁最终释放(即使发生异常)
if __name__ == "__main__":
t1 = threading.Thread(target=increment)
t2 = threading.Thread(target=increment)
t1.start()
t2.start()
t1.join()
t2.join()
print(f"最终count值:{count}") # 准确输出200000
3. 其他同步工具
(1)RLock(可重入锁)
Lock不允许同一线程多次acquire()(会导致死锁),而RLock支持同一线程多次获取锁,需保证acquire()与release()次数一致:rlock = threading.RLock()
rlock.acquire()
rlock.acquire() # 不会死锁
rlock.release()
rlock.release()
(2)Semaphore(信号量)
控制同时访问资源的线程数,适用于 “有限资源池” 场景(如最多 3 个线程同时访问数据库):
import threading
import time
sem = threading.Semaphore(3) # 最多3个线程同时执行
def task(name):
sem.acquire()
print(f"线程{name}获取资源,开始执行")
time.sleep(2)
sem.release()
print(f"线程{name}释放资源,执行完成")
if __name__ == "__main__":
for i in range(5):
t = threading.Thread(target=task, args=(i,))
t.start()
(3)Event(事件通知)
实现线程间的 “等待 - 通知” 机制,通过
set()设置事件、wait()等待事件、clear()清空事件:import threading
import time
event = threading.Event() # 初始为False
def wait_event():
print("线程等待事件触发...")
event.wait() # 阻塞,直到event被set()
print("事件触发,线程继续执行")
if __name__ == "__main__":
t = threading.Thread(target=wait_event)
t.start()
time.sleep(3)
print("主线程触发事件")
event.set() # 触发事件
t.join()
四、实战场景:多线程处理 I/O 密集型任务
多线程最适合 I/O 密集型任务(如批量下载文件、接口请求),因为 I/O 等待时线程会释放 GIL,让其他线程执行。
示例:多线程下载图片
import threading
import requests
import os
# 待下载的图片URL
img_urls = [
"https://example.com/img1.jpg",
"https://example.com/img2.jpg",
"https://example.com/img3.jpg"
]
save_dir = "downloads"
os.makedirs(save_dir, exist_ok=True)
def download_img(url, idx):
try:
resp = requests.get(url, timeout=10)
with open(f"{save_dir}/img{idx}.jpg", "wb") as f:
f.write(resp.content)
print(f"图片{idx}下载完成")
except Exception as e:
print(f"图片{idx}下载失败:{e}")
if __name__ == "__main__":
threads = []
for idx, url in enumerate(img_urls):
t = threading.Thread(target=download_img, args=(url, idx+1))
threads.append(t)
t.start()
# 等待所有下载线程完成
for t in threads:
t.join()
print("所有图片下载完成")
五、核心坑点与避坑指南
1. GIL 的 “陷阱”
- 误区:多线程能加速 CPU 密集型任务(如大规模计算);
- 真相:GIL 限制下,多线程在 CPU 密集型任务中反而因线程切换增加开销,效率低于单线程;
- 解决方案:CPU 密集型任务用
multiprocessing(多进程),I/O 密集型用threading或asyncio。
2. 线程安全问题
- 避免在多线程中修改非线程安全的对象(如列表、字典,虽可修改但需加锁);
- 优先使用
queue.Queue(线程安全队列)实现线程间数据传递,而非直接共享列表:import queue import threading q = queue.Queue() # 线程安全队列 def producer(): for i in range(5): q.put(i) print(f"生产数据:{i}") def consumer(): while not q.empty(): data = q.get() print(f"消费数据:{data}") q.task_done() # 标记任务完成 if __name__ == "__main__": t1 = threading.Thread(target=producer) t2 = threading.Thread(target=consumer) t1.start() t2.start() q.join() # 等待队列所有任务完成
3. 死锁问题
- 原因:多个线程互相等待对方持有的锁;
- 避坑:① 按固定顺序获取锁;② 给
acquire()设置超时时间;③ 尽量减少锁的嵌套。
4. 异常处理
- 子线程的异常不会自动传递到主线程,需手动捕获:
def task(): try: # 业务逻辑 1 / 0 except Exception as e: print(f"线程异常:{e}")
六、进阶优化:线程池(ThreadPoolExecutor)
threading模块无原生线程池,推荐使用concurrent.futures.ThreadPoolExecutor(基于threading封装),简化线程管理,避免手动创建大量线程:from concurrent.futures import ThreadPoolExecutor
import time
def task(name):
print(f"线程池任务{name}开始")
time.sleep(2)
return f"任务{name}完成"
if __name__ == "__main__":
# 创建线程池,最大线程数3
with ThreadPoolExecutor(max_workers=3) as executor:
# 提交任务并获取结果
futures = [executor.submit(task, i) for i in range(5)]
for future in futures:
print(future.result()) # 获取任务返回值
print("线程池所有任务完成")
总结
threading模块是 Python 多线程编程的核心,核心要点:- 适用场景:I/O 密集型任务(网络、文件、数据库操作);
- 核心问题:共享资源需加锁(
Lock/RLock),避免竞态条件; - 避坑重点:GIL 不适合 CPU 密集型任务,守护线程需注意生命周期,避免死锁;
- 进阶用法:优先使用
ThreadPoolExecutor管理线程池,简化开发。
浙公网安备 33010602011771号