asyncio.Lock、Event、Condition、Semaphore 有什么用?和 threading 里的同步原语有什么区别?
简化版
asyncio 也提供 Lock、Event、Condition、Semaphore 等同步原语,用来协调协程之间的共享状态和并发数量。它们的方法通常要 await,只适合在同一个事件循环的协程之间使用,不是线程安全工具;线程间同步应使用 threading 模块。
详细版
异步锁:
lock = asyncio.Lock()
async with lock:
shared.append(item)
异步信号量限流:
sem = asyncio.Semaphore(10)
async def fetch(url):
async with sem:
return await http_get(url)
常见用途:
Lock:保护协程共享状态;Event:通知一批协程某个条件已经发生;Condition:等待某个状态条件变化;Semaphore:限制并发数量,例如最多 10 个请求同时发出。
和 threading 的区别:
asyncio原语通过await挂起协程,不阻塞线程;- 通常不用于跨线程同步;
- 必须运行在事件循环语境中;
- 不要在 async 代码里使用阻塞式线程锁等待。
完整版教学
一、协程也会有共享状态问题
虽然事件循环通常单线程调度,但协程仍然会在 await 点切换。只要共享状态跨多个 await,就可能出现逻辑竞态。
例如:
if key not in cache:
value = await load_value(key)
cache[key] = value
多个协程可能同时发现 key 不存在,然后重复加载。此时需要同步策略。
二、asyncio.Lock 如何使用
异步锁的推荐写法:
lock = asyncio.Lock()
async with lock:
update_shared_state()
如果锁被占用,等待锁的协程会挂起,事件循环可以继续运行其他协程。不要用同步的 threading.Lock.acquire() 在事件循环里阻塞等待。
三、异步锁的临界区也要小
锁里不应该包太多慢操作。尤其是:
async with lock:
data = await slow_request()
shared.append(data)
这会让其他需要这把锁的协程长时间等待。更好的方式是:
data = await slow_request()
async with lock:
shared.append(data)
前提是慢请求不依赖锁内状态。临界区越短,并发越好。
四、Semaphore 是异步限流常用工具
异步程序很容易瞬间发出大量请求:
await asyncio.gather(*(fetch(url) for url in urls))
如果 urls 有几万个,可能打爆本机连接、远程服务或触发限流。可以用 Semaphore:
sem = asyncio.Semaphore(100)
async def limited_fetch(url):
async with sem:
return await fetch(url)
这表示最多 100 个请求同时处于受保护区域。
五、Event 和 Condition 的语义
Event 像一个异步开关:
event = asyncio.Event()
await event.wait()
event.set()
适合通知“初始化完成”“可以开始”“应该停止”。
Condition 更适合等待某个状态条件变化,通常和共享状态一起使用。和线程版本类似,被唤醒后仍要重新检查条件。
六、和 threading 原语不要混淆
threading.Lock 面向线程,asyncio.Lock 面向协程。它们解决的问题相似,但等待方式不同。
在 async 代码里使用同步锁,如果发生阻塞,整个事件循环可能被卡住。跨线程和跨协程混合场景要格外小心,通常需要明确边界:线程里用 threading 工具,协程里用 asyncio 工具,通过线程安全队列或 call_soon_threadsafe 等机制衔接。
七、常见误区与追问
| 原语 | 作用 | 典型用途 |
|---|---|---|
asyncio.Lock | 协程间互斥 | 保护共享状态的临界区 |
asyncio.Semaphore | 限制并发数量 | 限制接口请求、连接数 |
asyncio.Event | 广播状态通知 | 等初始化完成或开关打开 |
asyncio.Condition | 状态条件等待 | 等待复杂共享状态变化 |
- 误区:单线程事件循环里不会有共享状态问题。 协程会在
await处切换,只要共享状态跨多个步骤维护,就可能被其他协程插入。 - 误区:asyncio.Lock 可以在线程之间同步。 它面向同一事件循环中的协程,线程间同步应使用
threading模块的原语。 - 误区:Semaphore 只是性能优化工具。 它常用于保护外部资源,例如接口并发、数据库连接、文件句柄数量,避免把下游打爆。
- 追问:为什么异步临界区也要小? 持锁期间如果执行慢 I/O 或复杂逻辑,其他等待锁的协程都会被拖住。
- 追问:async with lock 比 acquire/release 好在哪里? 它能保证异常时释放锁,减少忘记释放造成的协程永久等待。
- 追问:什么时候不用锁而用队列? 如果问题是任务传递和单消费者串行处理,用
asyncio.Queue收敛状态通常比到处加锁更清楚。
记忆钩子:异步锁保护协程共享状态,信号量保护并发额度;它们不是线程锁的简单替身。
八、加强记忆
asyncio 同步原语是“不会阻塞事件循环的协程协调工具”。Lock 保护共享状态,Event 发信号,Condition 等条件,Semaphore 控制并发额度。它们不是线程锁,等待时要 await,使用边界要和 threading 原语分清。