c# 如何避免在lock中调用异步方法 c# 异步锁

来源:这里教程网 时间:2026-02-21 17:39:57 作者:

为什么不能在
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
只会让代码变重、难测、难调。

相关推荐