不能在 lock 中调用 await,因为 lock 依赖同线程加锁解锁,而 await 可能导致线程切换,使 Monitor.Exit() 抛出 SynchronizationLockException;应改用基于 SemaphoreSlim 的 AsyncLock。

为什么不能在 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 _releaser;
public AsyncLock()
{
_releaser = Task.FromResult((IDisposable)new Releaser(this));
}
public Task 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 只会让代码变重、难测、难调。










