async方法中禁用lock因线程切换致丢锁,应改用semaphoreslim或封装asynclock;需配try/finally或await using确保释放,注意锁粒度与取消令牌传递。

async 方法里不能用 lock,否则会卡死或丢锁
因为 lock 是同步原语,它会阻塞当前线程;而 await 可能导致线程切换,等异步操作回来时,早已不在原线程上执行,lock 早就释放了——临界区彻底裸奔。更糟的是,在 ASP.NET Core 等受限线程池环境里,这种阻塞可能快速耗尽线程,引发超时或 503。
典型错误写法:
public async Task DoSomethingAsync()
{
lock (_locker) // ❌ 编译不报错,但逻辑完全失效
{
await Task.Delay(100);
_sharedValue++;
}
}
- 编译器允许,但语义错误:
lock只管进入时加锁、离开时解锁,不管中间有没有await -
await后续代码恢复时,已脱离lock作用域,无锁保护 - 若在 UI 或 ASP.NET 上运行,还可能因同步上下文引发死锁
SemaphoreSlim 是异步锁的正确起点
SemaphoreSlim 是 .NET 原生支持异步等待的轻量同步原语,设为初始值 1、最大值 1 时,行为就等价于“异步互斥锁”。它没有线程绑定,WaitAsync() 返回 Task,可安全 await。
基础用法(必须配 try/finally):
private readonly SemaphoreSlim _sem = new(1, 1);
<p>public async Task UpdateSharedStateAsync()
{
await _sem.WaitAsync(); // ✅ 异步等待,不阻塞线程
try
{
await Task.Delay(100); // 模拟 I/O
_sharedValue++;
}
finally
{
_sem.Release(); // ✅ 必须确保释放,哪怕抛异常
}
}</p>
- 不要手动调用
Dispose()——SemaphoreSlim不需要显式释放资源(除非你长期持有且确定不再使用) - 避免在构造时传
0或负数,会导致WaitAsync()永远挂起 - 如果临界区极短(纯内存操作),考虑是否真需要异步锁;纯 CPU 工作用
lock更高效
封装成 AsyncLock 类让用法更安全
手写 try/finally 容易漏掉 Release(),尤其嵌套多层时。推荐封装一个 AsyncLock 类,返回 IDisposable,配合 using await 语法(C# 8+)自动释放。
最小可行实现:
public class AsyncLock
{
private readonly SemaphoreSlim _sem = new(1, 1);
<pre class="brush:php;toolbar:false;">public async Task<idisposable> LockAsync() =>
new Releaser(await _sem.WaitAsync(), _sem);
private struct Releaser : IDisposable
{
private readonly SemaphoreSlim _sem;
private readonly bool _needRelease;
public Releaser(bool acquired, SemaphoreSlim sem)
{
_needRelease = acquired;
_sem = sem;
}
public void Dispose()
{
if (_needRelease) _sem.Release();
}
}</idisposable>}
使用方式:
private readonly AsyncLock _asyncLock = new();
<p>public async Task ProcessAsync()
{
await using (await _asyncLock.LockAsync()) // ✅ 自动释放
{
await Task.Delay(100);
_sharedValue++;
}
}</p>
- 注意:必须用
await using,不是using;后者会同步调用Dispose,破坏异步语义 - 每个
AsyncLock实例应保护独立资源;别多个逻辑共用一个实例,否则串行化过度 - 不要把
AsyncLock当作“全局锁”滥用——它仍是进程内锁,跨进程需 Redis 或数据库行锁
容易被忽略的释放时机和并发粒度
很多人以为只要用了 SemaphoreSlim 就万事大吉,但实际出问题常发生在释放太早、或锁范围过大。
-
Release()必须严格对应一次WaitAsync()成功;重复释放会抛ObjectDisposedException或InvalidOperationException - 锁的粒度要匹配业务:比如对字典做
GetOrAdd,没必要锁整个字典,改用ConcurrentDictionary更合适 - 若临界区内有多个 await,且中间状态对外可见(如更新了某字段但未完成全部逻辑),其他协程可能看到“半成品”,这时光靠锁不够,得结合状态机或原子提交设计
-
SemaphoreSlim.WaitAsync(CancellationToken)支持取消,务必传入请求级CancellationToken,避免请求取消后还在排队等锁











