1. 什么是线程与 threading? #
- 进程是程序运行的容器,线程是进程内的执行单元,同一进程的线程共享内存。
- Python 程序启动时有一个主线程,默认代码都在主线程中顺序执行。
- 某步需要等待 I/O(网络、磁盘)时,后面的代码会被阻塞;多线程让等待期间其他任务继续执行。
threading是 Python 标准库,提供Thread、锁(Lock)、与queue.Queue配合的线程间通信。
注意 GIL: CPython 有全局解释器锁,多线程不能真正并行执行 CPU 计算,但适合 I/O 密集型任务(网络请求、文件读写),因为等待 I/O 时会释放 GIL。
2. 创建线程 #
- 最常用方式:
Thread(target=函数, args=(参数,)),然后start()启动、join()等待结束。 start()后线程独立运行,主线程用join()阻塞等待,确保子线程完成后再继续。- 多个线程可并行启动,各自
join()或使用列表批量join。 - 继承
Thread类重写run()也可以,但项目中传入函数的方式更简洁,一般够用。
# 导入 threading 模块
import threading
# 导入 time 模块,用于模拟耗时操作
import time
# 定义工作函数,接收线程名和延迟秒数
def worker(name, delay):
# 打印线程开始信息
print(f"线程 {name} 开始")
# 模拟耗时操作
time.sleep(delay)
# 打印线程结束信息
print(f"线程 {name} 结束")
# 创建线程 A,目标函数为 worker,参数为 ("A", 2)
t1 = threading.Thread(target=worker, args=("A", 2))
# 创建线程 B,延迟 1 秒
t2 = threading.Thread(target=worker, args=("B", 1))
# 启动线程 A
t1.start()
# 启动线程 B
t2.start()
# 等待线程 A 结束
t1.join()
# 等待线程 B 结束
t2.join()
# 所有子线程完成后打印提示
print("所有线程已完成")常用 API:
| 方法/属性 | 作用 |
|---|---|
start() |
启动线程 |
join(timeout) |
等待线程结束 |
is_alive() |
是否仍在运行 |
threading.current_thread() |
获取当前线程 |
daemon=True |
守护线程(程序退出时不等待) |
3. 线程同步:Lock #
- 多个线程同时修改同一变量会产生数据竞争,结果不可预期。
threading.Lock()保证同一时刻只有一个线程执行临界区代码。- 始终用
with lock:获取锁,异常时也会自动释放,避免死锁。 - 只要有多线程写共享数据,就必须加锁;只读共享数据一般无需锁。
# 导入 threading 模块
import threading
# 共享计数器,初始值为 0
counter = 0
# 创建互斥锁
lock = threading.Lock()
# 定义自增函数,循环累加 counter
def increment():
# 声明使用全局变量 counter
global counter
# 循环 10 万次
for _ in range(100_000):
# 获取锁后修改共享变量,确保原子性
with lock:
counter += 1
# 创建 10 个线程执行 increment
threads = [threading.Thread(target=increment) for _ in range(10)]
# 启动所有线程
for t in threads:
t.start()
# 等待所有线程结束
for t in threads:
t.join()
# 打印最终计数,应为 1000000
print(counter)4. 线程间通信:Queue #
- 线程之间传递数据,优先用
queue.Queue,它是线程安全的,无需手动加锁。 - 典型模式:生产者线程
put数据,消费者线程get数据。 Queue阻塞式get()会在队列为空时等待,适合生产者-消费者模型。- 比共享列表 + 锁更简单、更安全,是项目中的推荐做法。
# 导入 threading 模块
import threading
# 导入 queue 模块
import queue
# 导入 time 模块
import time
# 创建线程安全队列
q = queue.Queue()
# 生产者:向队列放入 5 条数据
def producer():
for i in range(5):
# 放入数据
q.put(f"数据-{i}")
# 模拟生产间隔
time.sleep(0.3)
# 放入 None 作为结束信号
q.put(None)
# 消费者:从队列取出并处理数据
def consumer():
while True:
# 阻塞等待获取数据
item = q.get()
# 收到结束信号则退出循环
if item is None:
break
# 打印消费的数据
print(f"消费 {item}")
# 启动生产者线程
threading.Thread(target=producer).start()
# 启动消费者线程
threading.Thread(target=consumer).start()5. GIL 与适用场景 #
- GIL(全局解释器锁)使 CPython 同一时刻只有一个线程执行 Python 字节码。
- I/O 密集型(网络、磁盘、数据库):多线程有效,等待 I/O 时释放 GIL,其他线程可运行。
- CPU 密集型(大量计算、图像处理):多线程无法加速,应使用
multiprocessing或多进程,见14.concurrent.md。 - 新手记住:网络爬虫、API 并发请求、批量文件读写 → 用 threading;纯计算 → 用多进程。
| 任务类型 | 推荐方案 | 原因 |
|---|---|---|
| 网络请求、文件 I/O | threading |
等待时释放 GIL |
| 大量数值计算 | multiprocessing |
绕过 GIL,真并行 |
| 高并发 I/O(大量连接) | asyncio 或线程池 |
见 concurrent 章节 |
6. 常见错误与注意事项 #
- 多线程修改共享变量不加锁,会导致计数错误、数据损坏等难以复现的 bug。
- 线程内异常不会自动传到主线程,目标函数内应自行
try/except并记录日志。 daemon=True的守护线程随主程序退出而终止,不要用它执行必须完成的任务。- 不要用多线程加速 CPU 密集计算,GIL 下反而可能更慢。
# 定义线程工作函数
def worker():
try:
# 执行可能出错的操作
do_something()
except Exception:
# 在线程内捕获并记录异常,避免静默失败
logger.exception("线程内出错")7. 总结 #
- threading 核心:
Thread(target=fn)→start()→join(),共享数据用Lock,传数据用Queue。 - 适合 I/O 密集型场景,不适合 CPU 密集型纯计算。
- 项目中最常用的两个模式:并发执行多个 I/O 任务、生产者-消费者队列。
- 更高级的线程池用法见
14.concurrent.md的ThreadPoolExecutor。
7.1 速查 #
# 导入 threading 和 Queue
import threading
from queue import Queue
# 创建线程并等待结束
t = threading.Thread(target=func, args=(arg,))
t.start()
t.join()
# 用锁保护共享数据
lock = threading.Lock()
with lock:
shared_var += 1
# 线程安全队列传数据
q = Queue()
q.put(item)
item = q.get()7.2 最佳实践 #
- 共享可变状态必须加锁,优先
with lock: - 线程间传数据用
queue.Queue,不要共享 list 不加锁 - I/O 并发用 threading,CPU 并行用 multiprocessing
- 线程函数内捕获异常并写日志,避免静默失败