人人都会AI编程

16.2 多线程:threading 模块、线程创建、线程同步、锁机制

更新时间:2026-07-12

在 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 等待任务分散到多个线程,让程序跑得更快、响应更及时。