死锁是多个线程因锁获取顺序不一致导致的循环等待,竞态条件是共享状态未受保护而依赖执行时序;二者均需通过统一锁序、原子操作封装、显式同步机制及运行时检测来防范。

Python并发编程中,死锁和竞态条件是最易被低估、却最常导致线上故障的两类问题。它们不总在测试中暴露,却可能在高并发、特定时序下突然引发数据错乱、服务卡死或响应超时。
死锁:多个线程互相等待对方释放锁
死锁不是“锁用多了”,而是锁的获取顺序不一致导致的循环等待。典型场景是两个线程分别持有A锁、B锁,又同时试图获取对方持有的锁。
例如:
线程1执行:lock_a.acquire() → lock_b.acquire()
线程2执行:lock_b.acquire() → lock_a.acquire()
一旦线程1拿到lock_a、线程2拿到lock_b,二者都会阻塞在第二步,永远无法继续。
避免方法:
立即学习“Python免费学习笔记(深入)”;
- 始终按**全局统一顺序**获取多个锁(如按锁对象id大小排序后依次acquire)
- 使用
threading.Lock的timeout参数(如lock.acquire(timeout=2)),失败后主动释放已持锁并重试 - 优先考虑
threading.RLock(可重入锁)或更高层抽象,如queue.Queue——它内部已处理同步,无需手动加锁
竞态条件:共享状态未受保护,执行结果依赖时序
竞态不是“多线程一定出错”,而是当读-改-写操作(如counter += 1)未原子化时,多个线程可能同时读到旧值、各自+1、再写回,导致一次更新丢失。
常见误判:
- 认为局部变量或函数内对象不会竞态(错误:若该对象被多个线程共享引用,仍会)
- 只给写操作加锁,忽略读操作也可能需要同步(尤其涉及复合判断,如
if not data: data = init()) - 用
list.append()等看似“简单”的操作代替锁(虽CPython中部分操作是原子的,但不保证逻辑原子性,且跨版本/解释器不可靠)
安全做法:
- 识别所有**跨线程共享的可变对象**(包括模块级变量、类属性、传入的可变参数)
- 对读-改-写操作,用锁包裹整个逻辑段,而非仅包裹赋值语句
- 用
threading.local()为每个线程提供独立副本,避免共享(适合配置、上下文等场景)
asyncio中的特殊陷阱:await不是线程安全的“保险丝”
协程并发不等于无锁。虽然asyncio是单线程事件循环,但以下情况仍会触发竞态:
- 多个协程修改同一全局变量或对象属性(如
shared_dict['count'] += 1) - 在
await前后状态不一致(如检查资源存在→await IO→资源被其他协程删除) - 误以为
asyncio.Lock能保护所有异步操作——它只阻塞协程调度,不阻止CPU密集型任务打断
建议:
- 对共享状态,显式使用
asyncio.Lock,且确保await lock.acquire()和lock.release()成对出现(推荐用async with lock:) - 避免在协程中直接操作线程共享对象;如需与线程交互,用
loop.run_in_executor()并做好同步 - 用
asyncio.create_task()替代await来实现真正的并发协作,而非串行等待
调试与检测:别等线上崩了才找问题
死锁和竞态往往难以复现。提前介入更有效:
- 用
threading.settrace()或sys.settrace()记录锁获取/释放轨迹(测试环境可用) - 启用
threading.Thread(daemon=True)前确认无关键锁未释放,否则主线程退出可能留下死锁残留 - 使用
pytest-asyncio配合asyncio.wait_for()设置超时,快速暴露挂起协程 - 对关键计数器、状态机,增加运行时断言(如
assert counter >= 0)或校验钩子










