为什么不能在 lock
中调用 await
因为
lock语句依赖线程所有权——它要求加锁和解锁必须发生在同一线程上。而
await可能导致线程切换(尤其在默认
SynchronizationContext或
TaskScheduler下),await 后续代码可能在另一个线程执行,此时
Monitor.Exit()会抛出
SynchronizationLockException:「对象同步方法被错误调用」。
用 AsyncLock
替代原生 lock
核心是改用基于
Task的可等待锁,常见做法是封装
SemaphoreSlim。它支持异步等待,且不绑定线程:
SemaphoreSlim.WaitAsync()是真正的异步等待,不会阻塞线程 初始化时传
1作为最大并发数,即可模拟互斥锁语义 务必配对使用
await _semaphore.WaitAsync()和
_semaphore.Release(),推荐用
try/finally保证释放
public class AsyncLock
{
private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1);
private readonly Task<IDisposable> _releaser;
public AsyncLock()
{
_releaser = Task.FromResult((IDisposable)new Releaser(this));
}
public Task<IDisposable> LockAsync()
{
var wait = _semaphore.WaitAsync();
return wait.IsCompleted ?
_releaser :
wait.ContinueWith((_, state) => (IDisposable)state, _releaser.Result, TaskScheduler.Default);
}
private void Release() => _semaphore.Release();
private struct Releaser : IDisposable
{
private readonly AsyncLock _toRelease;
public Releaser(AsyncLock toRelease) => _toRelease = toRelease;
public void Dispose() => _toRelease?.Release();
}
}
用法示例:
private readonly AsyncLock _asyncLock = new AsyncLock();
public async Task DoSomethingAsync()
{
using (await _asyncLock.LockAsync())
{
await File.WriteAllTextAsync("log.txt", DateTime.Now.ToString());
await Task.Delay(100); // 模拟其他异步操作
}
}
警惕 ConfigureAwait(false)
在锁上下文中的误用
即使你用了
AsyncLock,如果内部异步调用链中某处用了
.ConfigureAwait(false),而你又依赖
SynchronizationContext(比如在 WinForms/WPF UI 线程更新控件),仍可能出问题——但这不是锁的问题,是上下文丢失。
AsyncLock本身不依赖
SynchronizationContext,所以无需为它加
ConfigureAwait真正需要
ConfigureAwait(false)的,是锁**内部**那些纯计算或 I/O 异步操作(如数据库查询、HTTP 调用),避免无谓的上下文捕获开销 若锁内需更新 UI,则应在
await后显式切回 UI 线程,例如用
Control.Invoke()或
Dispatcher.Invoke()
不要用 Task.Run(() => { lock {} }) 伪装“异步锁”
这种写法看似绕过了线程限制,实则引入新问题:
把同步锁搬进线程池线程,无法控制并发粒度,容易压垮线程池 阻塞线程池线程违背 async/await 初衷,失去伸缩性 若锁内有await,一样会崩溃(因为
lock还在那个线程里) 性能更差:一次异步操作多了一次线程调度 + 同步锁争用
真要同步互斥 + 异步流程混合,优先拆分逻辑:把必须同步的部分(如修改共享字段)抽成小同步块,其余全走异步;或直接换用线程安全类型(
ConcurrentDictionary、
Interlocked等)。
最常被忽略的一点:很多所谓“需要异步锁”的场景,其实根本不需要锁——比如操作不同 key 的缓存、写入不同文件、调用不同 API 实例。先确认是否真有共享状态竞争,再决定加锁。盲目套
AsyncLock只会让代码变重、难测、难调。
