不能在异步方法中使用 lock,因其依赖线程绑定的 Monitor,而 await 可能切换线程导致死锁或异常;应改用 SemaphoreSlim.WaitAsync() 配合 try/finally 确保释放,且仅在 WaitAsync() 成功后调用一次 Release()。

为什么不能用 lock 配合 async/await
因为 lock 是基于线程同步原语(Monitor)实现的,它会阻塞当前线程;而 await 可能导致方法在不同线程上恢复执行,lock 无法跨 await 持有,直接编译报错:“不能在异步方法中使用 lock 语句”。强行用 Monitor.Enter + try/finally 也无效——await 后续代码可能在另一个线程运行,Monitor.Exit 会抛出 SynchronizationLockException。
SemaphoreSlim.WaitAsync() 的正确用法
SemaphoreSlim 是唯一被设计为支持异步等待的轻量级信号量,适用于资源池、并发限流、临界区保护等场景。关键点不是“能不能用”,而是“怎么用才不出错”:
- 必须始终配对调用
WaitAsync()和Release(),推荐用try/finally确保释放,不要依赖using(它不支持异步构造) -
WaitAsync()可接受CancellationToken,务必传入,否则超时或取消将无法中断等待 - 初始化时传入
initialCount == 1才等效于“异步互斥锁”;设为 >1 就是并发数控制,不是锁 - 避免在
static上长期持有——若某次Release()被遗漏,整个应用可能永久卡住
示例:
private static readonly SemaphoreSlim _lock = new SemaphoreSlim(1, 1);
public async Task DoWorkAsync()
{
await _lock.WaitAsync(_cancellationToken);
try
{
// 临界区:只允许一个调用者进入
await File.WriteAllTextAsync("log.txt", DateTime.Now.ToString());
}
finally
{
_lock.Release();
}
}
常见误用:WaitAsync() 后没 await 就直接进临界区
这是高频错误。写成 _lock.WaitAsync().ConfigureAwait(false) 但没 await,会导致后续代码立即执行,完全没加锁效果,且返回的 Task 被丢弃,异常也无法捕获。
另一种变体是误以为 await _lock.WaitAsync().ConfigureAwait(false) 中的 ConfigureAwait(false) 能优化性能——其实没必要:SemaphoreSlim 内部已做优化,且加锁本身不涉及 UI/上下文敏感逻辑,加了反而干扰可读性。
还要注意:如果临界区里抛出未处理异常,finally 仍会执行 Release(),这是正确行为;但若 WaitAsync() 自身抛出异常(如 OperationCanceledException),finally 不会触发,所以释放逻辑必须严格绑定在 WaitAsync() 成功之后。
比锁更合适的替代方案?
不是所有“需要串行”的场景都该用 SemaphoreSlim。比如:
- 纯内存状态更新(如计数器)→ 优先用
Interlocked系列方法,无锁且更快 - 需排队执行任务 → 考虑
Channel<T>+ 单消费者循环,语义更清晰,天然支持取消和背压 - 数据库行级冲突 → 应交给 DB 的
SELECT FOR UPDATE或乐观并发控制(rowversion),而非在应用层抢锁
真正需要 SemaphoreSlim 的典型场景很窄:必须保证同一时刻最多 N 个协程访问某外部资源(如 HTTP 客户端复用、文件句柄、第三方 SDK 实例),且该资源不支持内置并发控制。
最易被忽略的一点:SemaphoreSlim 不是线程安全的“释放计数器”——如果多次调用 Release() 超过当前计数,会抛出 InvalidOperationException。所以永远只在 WaitAsync() 成功后调用一次 Release(),别凭感觉多放。


















