c# 异步锁 AsyncLock 的实现和使用场景

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

为什么不能直接用
lock
做异步临界区保护

因为

lock
是同步原语,它会阻塞线程;而
await
可能导致线程切换,一旦在
lock
块里
await
,后续代码可能在另一个线程上执行,此时锁早已释放——
lock
完全失效,还可能引发死锁或竞态。常见错误现象是:看似加了锁,但并发写入仍发生,尤其在
Task.Run
或 I/O await 后。

AsyncLock
的核心实现靠
SemaphoreSlim

SemaphoreSlim
支持异步等待(
WaitAsync
),且可设初始计数为 1,正好模拟“互斥锁”。它比手写
TaskCompletionSource
+ 队列更可靠,也避免了
Monitor
无法跨 await 使用的问题。

关键点:

必须用
new SemaphoreSlim(1, 1)
初始化,确保仅允许一个持有者
务必在
finally
中调用
Release()
,否则锁永久泄漏
不要复用同一个
SemaphoreSlim
实例保护多个无关资源,否则造成不必要串行
public class AsyncLock
{
    private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1);
    public async ValueTask<IDisposable> LockAsync()
    {
        await _semaphore.WaitAsync();
        return new Releaser(_semaphore);
    }
    private struct Releaser : IDisposable
    {
        private readonly SemaphoreSlim _semaphore;
        public Releaser(SemaphoreSlim semaphore) => _semaphore = semaphore;
        public void Dispose() => _semaphore.Release();
    }
}

典型使用场景:共享资源的异步初始化与缓存更新

比如单例服务中延迟加载某个 HTTP 客户端配置、或刷新本地缓存时防止多个并发请求重复触发刷新逻辑。

常见误用:

在循环里反复
await lock.LockAsync()
而没提取出临界区最小粒度,拖慢整体吞吐
把整个 HTTP 调用包进锁里,本应只锁“判断是否需刷新 + 标记进行中”这两步 忘记
using
或未正确处理异常路径,导致
Dispose
没被调用
private readonly AsyncLock _refreshLock = new AsyncLock();
private DateTimeOffset _lastRefresh = DateTimeOffset.MinValue;
public async Task<T> GetCachedValueAsync()
{
    if (_lastRefresh.AddMinutes(5) < DateTimeOffset.Now)
    {
        using (await _refreshLock.LockAsync())
        {
            // 再次检查:防止其他协程已刷新
            if (_lastRefresh.AddMinutes(5) < DateTimeOffset.Now)
            {
                _cachedValue = await FetchFromApiAsync(); // 真正耗时操作
                _lastRefresh = DateTimeOffset.Now;
            }
        }
    }
    return _cachedValue;
}

性能与替代方案提醒

SemaphoreSlim.WaitAsync()
在无竞争时开销极小,但高并发下排队任务会堆积在内部队列,不如同步锁轻量。若临界区纯 CPU 密集且不 await,坚持用
lock
更高效。

更轻量的替代选择(需 .NET 6+):

AsyncReaderWriterLock
(第三方库如
Microsoft.Extensions.Caching.Memory
不提供,但
Nito.AsyncEx
有)适合读多写少
对简单标志位控制,可用
Interlocked.CompareExchange
+ 循环重试,避免锁开销
真正需要协调多个异步操作完成顺序时,考虑
TaskCompletionSource
Channel<t></t>
,而非强行套用
AsyncLock

最容易被忽略的一点:AsyncLock 不解决分布式场景问题——它只在单进程内有效。跨服务或跨机器的并发控制,得靠 Redis 锁、数据库行锁或专门的协调服务。

相关推荐