在 Python 中想让多个任务“看起来同时执行”,最直接的办法就是使用多线程。这一节我们就聚焦 threading 模块,讲清楚如何创建线程、为什么需要同步,以及锁的具体用法。
什么时候用多线程?
多线程最适合 I/O 密集型 场景:网络请求、文件读写、数据库查询、等待用户输入等。在这些任务中,程序大部分时间都在等外部的响应,线程切换可以充分利用这段等待时间。
如果你面对的是 CPU 密集型 任务(视频编解码、大规模数学运算),多线程往往受制于 GIL(全局解释器锁),无法实现真正的并行计算,此时更适合用多进程(16.3 节介绍)。
线程创建
最简单的线程创建方式是实例化 threading.Thread,并指定一个目标函数。
import threading
import time
def worker(name, delay):
"""模拟一个需要耗时的任务"""
print(f"线程 {name} 开始工作")
time.sleep(delay) # 模拟 I/O 等待
print(f"线程 {name} 完成")
# 创建两个线程,target 是要执行的函数,args 是传参
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("所有线程结束")
运行结果可能为(因调度不确定,顺序可能不同):
线程 A 开始工作
线程 B 开始工作
线程 B 完成 # 因为 delay 短,先结束
线程 A 完成
所有线程结束
另一种方式是继承 Thread 类,重写 run 方法,适用于更复杂的线程逻辑,但日常开发中传函数更简洁。
为什么需要线程同步?
当多个线程同时访问同一个共享数据(比如一个列表、一个全局计数器)时,可能出现数据错乱——经典的 竞态条件 问题。
看一个没有同步的计数器例子:
import threading
count = 0
def increase():
global count
for _ in range(1000000):
count += 1 # 这并非一个原子操作
t1 = threading.Thread(target=increase)
t2 = threading.Thread(target=increase)
t1.start()
t2.start()
t1.join()
t2.join()
print(count) # 预期是 2000000,实际往往更小
因为 count += 1 在底层被分解为“读取、计算、写回”三步,线程可能在读取后被其他线程打断,导致“丢失”部分更新。所以需要同步机制确保同一时刻只有一个线程操作共享数据。
锁机制
threading.Lock 是最基础的同步工具,像一个“令牌”,拿到令牌的线程才能进入临界区。
import threading
count = 0
lock = threading.Lock()
def increase():
global count
for _ in range(1000000):
with lock: # 获取锁,离开 with 块自动释放
count += 1 # 保证同一时刻只有一个线程执行这里
t1 = threading.Thread(target=increase)
t2 = threading.Thread(target=increase)
t1.start()
t2.start()
t1.join()
t2.join()
print(count) # 一定是 2000000
with lock: 是推荐写法,等价于手动调用 lock.acquire() 和 lock.release(),无论是否发生异常都能确保释放,避免死锁。
锁的注意事项:
- 锁的粒度要尽量小,只保护必要的临界区,否则可能削弱多线程带来的性能优势。
- 多个锁嵌套使用时注意获取顺序,避免不同线程互相等待对方释放而死锁。实在需要获取多个锁时,可以统一使用
threading.RLock(可重入锁)并保持一致的获取顺序。 - 不要过分依赖锁,优先考虑是否可以通过设计避免共享状态(例如用队列
queue.Queue在线程间安全传递数据)。
线程同步其他工具
除了普通锁,threading 还提供了几种高级同步原语:
- RLock(可重入锁):同一个线程可以多次获取,不会把自己锁死,在递归或嵌套调用的场景中比 Lock 更安全。
- Semaphore(信号量):允许指定数量线程同时访问,常用于限制并发资源(如数据库连接池)。
- Event(事件):一个线程发出信号,其他线程等待该信号。适合“等待某个条件触发”的协调模式。
- Condition(条件变量):更灵活的等待/通知,可以让线程在满足特定条件时才继续执行,生产者-消费者模式中常用。
实用建议
- 简单任务直接用
Thread(target=func)创建线程,配合join等待结束。 - 共享数据加锁保护,用
with lock:保证异常安全。 - 多线程调试困难,尽可能保持线程逻辑简单、共享状态明确。当一个功能变得复杂紊乱时,可能就是改用
concurrent.futures.ThreadPoolExecutor(线程池)或asyncio协程的好时机。
掌握了这些,你就可以放心地把 Python 里的 I/O 等待任务分散到多个线程,让程序跑得更快、响应更及时。