← 返回题目列表

asyncio.Lock、Event、Condition、Semaphore 有什么用?和 threading 里的同步原语有什么区别?

高频 中等 第 5 / 27 题 更新于 2026/07/27
asyncio.LockSemaphore异步同步

简化版

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 原语分清。