Python threading 使用指南:并发编程的轻骑兵
作者:书到用时方恨少!
发布日期:2026年4月2日
阅读时长:约19分钟
📌 前言
在 Python 并发编程的世界里,threading 模块如同一匹轻骑兵------轻量、灵活、响应迅速。它允许你在单个进程中创建多个线程,共享同一块内存空间,特别适合 I/O 密集型任务 (如网络爬虫、文件读写、数据库交互)。然而,由于全局解释器锁(GIL)的存在,threading 并不能让 CPU 密集型任务真正并行,但这并不妨碍它成为提升程序响应速度和资源利用率的利器。
无论你是想编写一个不卡顿的 GUI 程序,还是需要并发处理成千上万个网络请求,这篇博客都将带你从零开始,深入理解 threading 的核心概念、同步机制、通信方式以及最佳实践。让我们一同探索并发编程的奥秘!
1. 🧵 线程是什么?为什么需要线程?
1.1 进程与线程
- 进程:操作系统资源分配的基本单位,拥有独立的内存空间、文件句柄等。进程间相互隔离,通信成本高。
- 线程:CPU 调度的基本单位,隶属于进程。同一个进程内的线程共享内存空间和资源,创建和切换成本远低于进程。
可以这样理解:进程就像一家公司,线程就是公司里的员工。员工共享公司的办公区、设备,可以高效协作;而不同公司之间的资源不共享,沟通需要额外的渠道(如邮件)。
1.2 GIL(全局解释器锁)------ Python 线程的"紧箍咒"
CPython(官方 Python 实现)中有一个 GIL,它确保同一时刻只有一个线程执行 Python 字节码。这意味着即使在多核 CPU 上,Python 的多线程也无法真正并行执行计算密集型任务。
但这是否意味着 threading 毫无用处? 绝对不是!
- I/O 密集型任务:当线程执行 I/O 操作(如网络请求、文件读写、用户输入)时,会释放 GIL,其他线程可以趁机运行。因此多线程可以极大地提高 I/O 密集型程序的吞吐量。
- 响应性:即使有一个线程在计算,另一个线程也可以处理用户界面事件,防止程序"假死"。
选型小贴士:
- I/O 密集型 → 优先考虑
threading或asyncio - CPU 密集型 → 使用
multiprocessing - 高并发网络服务 →
asyncio可能更高效
2. 🚀 快速入门:创建并启动线程
threading 模块提供了 Thread 类,你可以通过两种方式创建线程。
2.1 方式一:传入目标函数
python
import threading
import time
def worker(name, delay):
print(f"线程 {name} 开始")
time.sleep(delay)
print(f"线程 {name} 结束")
# 创建线程
t1 = threading.Thread(target=worker, args=("A", 2))
t2 = threading.Thread(target=worker, args=("B", 1))
# 启动线程
t1.start()
t2.start()
# 等待线程结束
t1.join()
t2.join()
print("所有线程执行完毕")
target:可调用对象(函数)。args:位置参数元组。kwargs:关键字参数字典。start():启动线程(实际开始运行)。join([timeout]):等待线程结束(阻塞主线程)。
2.2 方式二:继承 Thread 类
python
import threading
import time
class MyThread(threading.Thread):
def __init__(self, name, delay):
super().__init__()
self.name = name
self.delay = delay
def run(self): # 重写 run 方法
print(f"线程 {self.name} 开始")
time.sleep(self.delay)
print(f"线程 {self.name} 结束")
t1 = MyThread("A", 2)
t2 = MyThread("B", 1)
t1.start()
t2.start()
t1.join()
t2.join()
2.3 守护线程(Daemon Thread)
守护线程在主线程结束时自动终止(不等待其完成)。设置 daemon=True 即可。
python
def daemon_task():
while True:
print("守护线程运行中...")
time.sleep(1)
d = threading.Thread(target=daemon_task, daemon=True)
d.start()
time.sleep(3)
print("主线程结束,守护线程也随之结束")
3. 🤝 线程同步:防止数据混乱
多个线程共享内存,同时修改同一变量会导致数据竞争(race condition)。threading 提供了多种同步原语。
3.1 Lock(互斥锁)
最基础的锁,一次只允许一个线程持有。
python
import threading
counter = 0
lock = threading.Lock()
def increment():
global counter
for _ in range(100000):
with lock: # 上下文管理器自动获取和释放
counter += 1
threads = []
for _ in range(5):
t = threading.Thread(target=increment)
threads.append(t)
t.start()
for t in threads:
t.join()
print(counter) # 应该是 500000(如果没有锁,结果会小于此值)
acquire(blocking=True):获取锁。release():释放锁。with lock:是最推荐的用法,避免忘记释放。
3.2 RLock(可重入锁)
允许同一线程多次获取锁,适用于递归调用或同一线程需要重复加锁的场景。
python
rlock = threading.RLock()
rlock.acquire()
rlock.acquire() # 同一线程可以再次获取
rlock.release()
rlock.release()
3.3 Semaphore(信号量)
控制同时访问资源的线程数量。常用于限制数据库连接数或并发下载数量。
python
sem = threading.Semaphore(3) # 最多允许 3 个线程同时访问
def limited_task():
with sem:
print(f"{threading.current_thread().name} 正在运行")
time.sleep(1)
for i in range(10):
threading.Thread(target=limited_task).start()
3.4 Event(事件)
用于线程间的信号通知:一个线程设置事件,其他线程等待该事件。
python
event = threading.Event()
def waiter():
print("等待事件...")
event.wait()
print("事件已发生,继续执行")
def setter():
time.sleep(2)
print("设置事件")
event.set()
threading.Thread(target=waiter).start()
threading.Thread(target=setter).start()
wait([timeout]):阻塞直到事件被设置。set():设置事件(唤醒所有等待线程)。clear():清除事件标志。
3.5 Condition(条件变量)
比 Event 更灵活,允许线程等待某个条件满足后再继续。常用于生产者-消费者模式。
python
import threading
import time
condition = threading.Condition()
items = []
def producer():
for i in range(5):
with condition:
items.append(i)
print(f"生产 {i}")
condition.notify() # 通知一个等待的消费者
time.sleep(0.5)
def consumer():
while True:
with condition:
while not items:
condition.wait() # 等待被通知
item = items.pop(0)
print(f"消费 {item}")
time.sleep(1)
threading.Thread(target=producer).start()
threading.Thread(target=consumer).start()
3.6 Barrier(屏障)
等待所有线程都到达某个点后才一起继续执行。
python
barrier = threading.Barrier(3)
def worker():
print(f"{threading.current_thread().name} 准备就绪")
barrier.wait()
print(f"{threading.current_thread().name} 继续执行")
for i in range(3):
threading.Thread(target=worker).start()
4. 📨 线程间通信:Queue 队列
直接共享变量需要繁琐的锁,而 queue.Queue 提供了线程安全的队列,是生产者-消费者模式的首选。
python
import threading
import queue
import time
q = queue.Queue(maxsize=10)
def producer():
for i in range(20):
q.put(i)
print(f"生产 {i}")
time.sleep(0.2)
def consumer():
while True:
item = q.get()
if item is None: # 毒丸(poison pill)信号
break
print(f"消费 {item}")
q.task_done() # 标记任务完成
print("消费者退出")
# 启动生产者
threading.Thread(target=producer).start()
# 启动多个消费者
for _ in range(3):
threading.Thread(target=consumer, daemon=True).start()
# 等待所有生产任务完成
q.join()
# 发送结束信号
for _ in range(3):
q.put(None)
Queue(maxsize=0):先进先出队列。LifoQueue:后进先出(栈)。PriorityQueue:优先级队列。put(item, block=True, timeout=None):放入元素。get(block=True, timeout=None):取出元素。task_done():通知队列一个任务已完成。join():等待所有任务完成(即task_done调用次数等于put次数)。
5. 🧰 线程池:高效管理大量线程
手动创建大量线程开销较大,使用线程池可以复用线程,提高性能。Python 标准库提供了 concurrent.futures.ThreadPoolExecutor。
python
from concurrent.futures import ThreadPoolExecutor
import time
def task(n):
time.sleep(1)
return n * n
with ThreadPoolExecutor(max_workers=4) as executor:
# 方式1:map,保持顺序
results = executor.map(task, range(10))
print(list(results))
# 方式2:submit + futures
futures = [executor.submit(task, i) for i in range(10)]
for f in futures:
print(f.result())
为什么使用线程池?
- 减少线程创建销毁的开销。
- 限制并发数量,防止资源耗尽。
- 简化异常处理和结果收集。
6. 💡 实战案例
案例一:并发下载多个网页
python
import threading
import requests
urls = [
"https://www.python.org",
"https://www.github.com",
"https://www.stackoverflow.com",
# ... 更多 URL
]
results = {}
lock = threading.Lock()
def fetch_url(url):
try:
resp = requests.get(url, timeout=5)
with lock:
results[url] = resp.status_code
print(f"完成: {url} - {resp.status_code}")
except Exception as e:
with lock:
results[url] = str(e)
threads = []
for url in urls:
t = threading.Thread(target=fetch_url, args=(url,))
t.start()
threads.append(t)
for t in threads:
t.join()
print(results)
案例二:多线程批量处理文件(模拟)
python
import threading
import os
def process_file(filepath):
# 模拟文件处理(例如统计行数、计算哈希等)
with open(filepath, 'r') as f:
lines = len(f.readlines())
print(f"{filepath}: {lines} 行")
def main():
files = [f for f in os.listdir('.') if f.endswith('.txt')]
threads = []
for file in files:
t = threading.Thread(target=process_file, args=(file,))
t.start()
threads.append(t)
for t in threads:
t.join()
if __name__ == "__main__":
main()
案例三:带超时的线程等待
python
import threading
import time
def long_task():
time.sleep(10)
print("任务完成")
t = threading.Thread(target=long_task)
t.start()
t.join(timeout=3) # 最多等待 3 秒
if t.is_alive():
print("任务超时,仍在运行中")
# 注意:无法强制终止线程,只能设计协作式退出机制
7. ⚠️ 常见陷阱与注意事项
7.1 死锁(Deadlock)
多个线程互相等待对方释放资源,导致程序永久阻塞。
避免方法:
- 使用
with语句自动释放锁。 - 按固定顺序获取锁。
- 使用
threading.RLock或超时机制。
7.2 线程无法强制终止
Python 线程没有 terminate() 方法。通常通过标志位或队列的毒丸来让线程主动退出。
python
stop_flag = threading.Event()
def worker():
while not stop_flag.is_set():
# 执行任务
pass
t = threading.Thread(target=worker)
t.start()
stop_flag.set() # 通知线程退出
t.join()
7.3 全局解释器锁(GIL)对 CPU 密集型任务的影响
对于纯计算任务,多线程甚至比单线程更慢(因为锁竞争)。请使用 multiprocessing。
7.4 避免使用 threading.active_count() 进行精确控制
它返回的是当前活动线程数(包括已启动但未完成的),不保证完全准确。
7.5 线程安全的数据结构
queue.Queue是线程安全的。- 列表、字典等内置容器不是线程安全的,需要加锁保护。
7.6 异常处理
线程中的异常不会传播到主线程,务必在线程函数内部捕获并处理。
python
def safe_worker():
try:
# 可能出错的代码
pass
except Exception as e:
print(f"线程出错: {e}")
8. 📊 threading vs asyncio vs multiprocessing
| 特性 | threading | asyncio | multiprocessing |
|---|---|---|---|
| 并发模型 | 多线程(抢占式) | 协程(协作式) | 多进程 |
| 适合任务 | I/O 密集型 | 高并发 I/O(网络) | CPU 密集型 |
| 真正的并行 | 否(GIL限制) | 否 | 是 |
| 内存开销 | 低(共享) | 极低 | 高 |
| 编程复杂度 | 中等(需处理锁) | 较高(async/await) | 中等(IPC) |
| 第三方库支持 | 广泛 | 需要异步版本 | 广泛 |
选型建议:
- 简单并发 I/O,如少量爬虫 →
threading - 成千上万个网络连接 →
asyncio - 数值计算、图像处理 →
multiprocessing - GUI 程序 →
threading(保持界面响应)
9. 🎯 总结
通过本文,我们全面学习了 Python threading 模块:
- ✅ 线程基础:创建、启动、守护线程、join
- ✅ 同步机制:Lock、RLock、Semaphore、Event、Condition、Barrier
- ✅ 线程间通信:queue.Queue 安全传递数据
- ✅ 线程池 :
ThreadPoolExecutor高效管理 - ✅ 实战案例:网页下载、文件处理、超时控制
- ✅ 注意事项:死锁、GIL、无法强制终止等
threading 是 Python 并发编程的基石之一,掌握它将使你能够编写更高效、更响应的应用程序。但请记住,没有银弹------根据任务类型选择合适的并发工具,才能发挥最大的威力。
希望这篇博客能帮助你理清 threading 的脉络。如果在实践中遇到有趣的并发问题,欢迎在评论区讨论!感谢阅读,我们下篇见!🚀