threading.Lock可防止多线程竞态条件,其核心是确保共享变量(如counter)的“读—改—写”三步原子执行;推荐用with语句自动管理锁,避免手动acquire/release遗漏,且锁粒度应最小化。

用 threading.Lock 防止多线程写共享变量出错
竞态条件最常见于多个线程同时读写同一变量,比如计数器 counter。不加保护时,counter += 1 这种看似原子的操作实际包含「读—改—写」三步,线程可能在中间被打断,导致结果丢失。
正确做法是显式加锁:
import threading <p>counter = 0 lock = threading.Lock()</p><p>def increment(): global counter for _ in range(100000): with lock: # 自动 acquire/release counter += 1
注意:threading.Lock 是线程级的,不能跨进程生效;若用 multiprocessing,得换 multiprocessing.Lock。
- 别手动调用
lock.acquire()后忘记release(),优先用with语句 - 锁粒度要合理:锁整个函数体可能严重拖慢性能;只锁真正共享修改的几行更稳妥
- 避免嵌套锁或交叉加锁顺序,否则容易死锁
用 asyncio.Lock 处理协程并发写共享状态
异步场景下,threading.Lock 不起作用——它会阻塞整个 event loop。必须用 asyncio.Lock,且所有访问都得是 await 操作。
立即学习“Python免费学习笔记(深入)”;
典型错误是混用同步/异步锁,或在 await 前就进了临界区:
import asyncio
<p>shared_state = {"value": 0}
lock = asyncio.Lock()</p><p>async def update_state():
async with lock:</p><h1>此处才是安全的临界区</h1><pre class="brush:php;toolbar:false;"> current = shared_state["value"]
await asyncio.sleep(0.001) # 模拟异步 I/O,但不破坏锁保护
shared_state["value"] = current + 1-
asyncio.Lock只能用于async def函数内,且必须配合async with - 不要在锁内做 CPU 密集型操作(如大循环、复杂计算),会阻塞其他协程
- 如果共享对象本身支持异步操作(如
aioredis),优先用其内置原子命令,而非自己加锁
用数据库事务或原子操作替代应用层锁
很多竞态其实发生在数据持久化环节,比如「查余额 → 扣款 → 更新」三步之间被并发请求插队。这时候在 Python 层加锁只是治标,还可能因服务重启失效。
更可靠的方式是把一致性保障下沉到数据库:
- 用
SELECT ... FOR UPDATE(PostgreSQL/MySQL InnoDB)锁定行,再更新 - 用带条件的
UPDATE ... WHERE version = ?实现乐观锁 - 对计数类场景,直接用
UPDATE table SET count = count + 1 WHERE id = ?,依赖数据库自身原子性
Python ORM 如 SQLAlchemy 支持 session.refresh() 和 for_update(),但要注意隔离级别(如 READ COMMITTED 不足以防止幻读)。
为什么 queue.Queue 比手动锁 list 更安全
新手常试图用 list + threading.Lock 实现生产者-消费者,但容易漏掉边界检查或异常路径。而 queue.Queue 是线程安全的,内部已封装锁和条件变量。
例如,用 queue.Queue 传递任务比手写 append()/pop(0) 加锁更简洁可靠:
from queue import Queue import threading <p>task_queue = Queue()</p><p>def worker(): while True: item = task_queue.get() # 自动线程安全,且阻塞等待 if item is None: break process(item) task_queue.task_done()
-
Queue的get()、put()、task_done()全部线程安全 - 它还内置了阻塞、超时、满/空信号机制,比裸锁 +
time.sleep()轮询健壮得多 - 但注意:它只保证操作原子性,不保证你塞进去的对象本身线程安全
真正难的不是加锁,而是识别哪些状态确实需要保护——经常被忽略的是缓存、单例实例、全局配置字典,这些地方一旦并发修改,问题会延迟暴露。


















