async方法中不能用lock,因await会导致锁提前释放;应使用semaphoreslim(1,1)或自定义asynclock实现异步互斥,多资源场景需按key分锁并注意生命周期管理。

async方法里不能用lock,否则锁会提前释放
这是最常踩的坑:在lock块里写await,编译器不会报错,但运行时锁会在await返回前就释放。因为lock基于Monitor,只绑定当前同步上下文;一旦遇到await,线程可能切换、状态机挂起,Monitor.Exit早已执行完毕。
典型错误模式:
private static readonly object _lockObj = new();
public async Task DoWorkAsync()
{
lock (_lockObj) // ← 这里加锁
{
await Task.Delay(100); // ← await后锁已释放!
UpdateSharedState(); // ← 此处不再受保护
}
}
- 现象:多个并发调用
DoWorkAsync时,UpdateSharedState被同时执行 - 根本原因:
lock不是异步感知的,它不等待await完成才退出 - 别指望加
ConfigureAwait(false)能修复——这解决的是上下文捕获问题,不是锁生命周期问题
SemaphoreSlim(1, 1)是最轻量、最可靠的异步互斥方案
不需要引入第三方库,.NET Framework 4.5+ 和 .NET Core / .NET 5+ 原生支持SemaphoreSlim,设为初始值1即等效于互斥锁。
正确用法必须配对WaitAsync和Release,且Release必须放在finally中:
private static readonly SemaphoreSlim _semaphore = new(1, 1);
public async Task DoWorkAsync()
{
await _semaphore.WaitAsync(); // ← 异步等待,不阻塞线程池
try
{
await Task.Delay(100);
UpdateSharedState();
}
finally
{
_semaphore.Release(); // ← 必须放这里,哪怕抛异常也要释放
}
}
- 性能:比
Mutex快一个数量级,无内核对象开销 - 注意:不要重复
new SemaphoreSlim(1, 1)——它是有状态的,应复用静态实例或按资源粒度单例管理 - 不推荐用
using包裹WaitAsync返回值,因为SemaphoreSlim本身不实现IDisposable语义来自动释放
自己封装AsyncLock时,TaskCompletionSource比SemaphoreSlim更可控
如果你需要精确控制等待队列、支持取消、或避免SemaphoreSlim内部计数器的潜在竞态(极少数高并发场景),可用TaskCompletionSource手写一个轻量AsyncLock。
核心逻辑是维护一个bool _isLocked标识 + Queue<taskcompletionsource>></taskcompletionsource>等待队列:
public class AsyncLock
{
private readonly object _lock = new();
private bool _isLocked;
private readonly Queue<taskcompletionsource>> _waiters = new();
public async Task<idisposable> LockAsync(CancellationToken ct = default)
{
var tcs = new TaskCompletionSource<object>();
lock (_lock)
{
if (!_isLocked)
{
_isLocked = true;
tcs.SetResult(null);
}
else
{
_waiters.Enqueue(tcs);
}
}
await tcs.Task.WaitAsync(ct).ConfigureAwait(false);
return new Releaser(this);
}
private void Release()
{
TaskCompletionSource<object> next = null;
lock (_lock)
{
if (_waiters.Count > 0)
next = _waiters.Dequeue();
else
_isLocked = false;
}
next?.SetResult(null);
}
private class Releaser : IDisposable
{
private readonly AsyncLock _lock;
public Releaser(AsyncLock @lock) => _lock = @lock;
public void Dispose() => _lock.Release();
}
}</object></object></idisposable></taskcompletionsource>
- 优势:完全用户态,无系统调用;可轻松注入日志、超时、优先级等逻辑
- 风险:手写易出错,比如忘记
lock(_lock)保护队列操作,或SetResult前未检查CancellationToken - 适用场景:UI线程调度、测试模拟、或已有复杂协调逻辑需嵌入锁流程中
多资源隔离时,别用全局SemaphoreSlim,改用ConcurrentDictionary按Key分锁
例如硬件端口COM1、COM2需独立互斥,但共用同一套锁机制。此时若只用一个SemaphoreSlim,所有端口会串行化,严重降低吞吐。
标准解法是用ConcurrentDictionary<string semaphoreslim></string>动态管理:
private static readonly ConcurrentDictionary<string semaphoreslim> _portLocks =
new();
public async Task AccessPortAsync(string port)
{
var semaphore = _portLocks.GetOrAdd(port, _ => new SemaphoreSlim(1, 1));
await semaphore.WaitAsync();
try
{
await HardwareIOAsync(port);
}
finally
{
semaphore.Release();
// 可选:空闲超时后清理,防内存泄漏
if (semaphore.CurrentCount == 1 && _portLocks.TryRemove(port, out _))
semaphore.Dispose();
}
}</string>
- 关键点:
GetOrAdd是线程安全的,避免重复创建 - 陷阱:不清理会导致
SemaphoreSlim实例无限堆积;建议配合WeakReference或定时扫描做回收 - 替代方案:用
Lazy<semaphoreslim></semaphoreslim>包装,但无法动态销毁,适合固定Key集合
HttpClient实例加锁,不如按baseUrl分锁;又比如在IoT设备管理中,把设备ID作为锁Key,却忘了设备下线后锁没释放——这些才是线上事故的根源。











