C#.NET AsyncLock 完全解析:async/await 下的并发控制方案

简介

AsyncLock 是一种自定义的异步互斥锁(Mutex Lock),专为异步编程场景设计,用于在 async/await 方法中实现线程安全的互斥访问。它弥补了 .NET 中传统 lock 语句(基于 Monitor)的不足,因为 lock 是同步阻塞的,在异步环境中会阻塞线程池线程,导致性能下降或死锁风险。

  • 核心原理:AsyncLock 通常基于 SemaphoreSlim(1, 1) 实现,允许异步等待锁的获取,而不阻塞当前线程。等待的任务会被挂起(suspend),释放线程池资源,支持 CancellationToken 取消操作。

  • 来源:.NET 标准库中没有内置 AsyncLock,通常通过 NuGetNito.AsyncEx(由 `Stephen Cleary 维护)使用。该库提供了生产就绪的实现。

使用方式:

csharp 复制代码
private readonly AsyncLock _mutex = new AsyncLock();

public async Task DoWorkAsync()
{
    using (await _mutex.LockAsync())
    {
        await Task.Delay(100);
    }
}

为什么需要 AsyncLock?

在异步编程中,共享资源(如文件、数据库或 UI 更新)需要互斥访问:

  • 传统 lock 的问题:lock 会阻塞调用线程,如果在 async 方法中使用,会导致线程池耗尽,尤其在高并发场景(如 Web APITCP 处理)中。

  • AsyncLock 的优势:非阻塞等待,使用 await 挂起任务,适合 I/O 密集型操作(如网络请求、文件读写)。

  • 适用场景:

    • 异步方法中保护共享状态(如缓存更新)。

    • UI 线程与后台任务的同步。

    • 避免死锁的并发控制。

普通 lock 的问题

在同步代码中,我们通常用 lock 来保护临界区:

csharp 复制代码
private readonly object _syncRoot = new object();

public void Increment()
{
    lock (_syncRoot)
    {
        _count++;
    }
}

但是在异步代码中:

csharp 复制代码
public async Task IncrementAsync()
{
    lock (_syncRoot)
    {
        await SomeAsyncOperation(); // ❌ 编译错误
    }
}

lock 不能与 await 一起使用,因为:

  • await 会让出线程控制权;

  • 离开 lock 作用域时会立即释放锁;

  • 这会破坏线程安全。

AsyncLock 的基本思想

核心目标是实现 异步安全的锁,使得:

  • 异步任务按顺序进入临界区;

  • 释放时能唤醒下一个等待者;

  • 不阻塞线程(不像 lock 会阻塞)。

基本原理

可以用 SemaphoreSlim(轻量信号量)实现:

csharp 复制代码
public sealed 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, CancellationToken.None,
                                TaskContinuationOptions.ExecuteSynchronously,
                                TaskScheduler.Default);
    }

    private sealed class Releaser : IDisposable
    {
        private readonly AsyncLock _toRelease;

        internal Releaser(AsyncLock toRelease) => _toRelease = toRelease;

        public void Dispose()
        {
            _toRelease._semaphore.Release();
        }
    }
}

使用示例

csharp 复制代码
private readonly AsyncLock _lock = new AsyncLock();
private int _count = 0;

public async Task IncrementAsync()
{
    using (await _lock.LockAsync())
    {
        _count++;
        await Task.Delay(100); // 模拟异步操作
        Console.WriteLine($"Count: {_count}");
    }
}

调用示例

csharp 复制代码
var tasks = Enumerable.Range(0, 5).Select(_ => IncrementAsync());
await Task.WhenAll(tasks);

输出将是:

makefile 复制代码
Count: 1
Count: 2
Count: 3
Count: 4
Count: 5

所有操作顺序执行,没有并发问题。

与 SemaphoreSlim 的区别

特性 SemaphoreSlim AsyncLock
可同时进入的任务数 可指定 (n) 永远只允许 1
使用方式 WaitAsync/Release using(await LockAsync())
使用便捷性 稍复杂 简洁且自动释放
推荐场景 控制并发数量 异步临界区互斥

改进版:支持 CancellationToken

可以进一步增强:

csharp 复制代码
public async Task<IDisposable> LockAsync(CancellationToken cancellationToken)
{
    await _semaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
    return new Releaser(_semaphore);
}

异步文件写入

csharp 复制代码
public class AsyncFileWriter
{
    private readonly AsyncLock _lock = new AsyncLock();
    private readonly string _filePath;

    public AsyncFileWriter(string path) => _filePath = path;

    public async Task WriteAsync(string message)
    {
        using (await _lock.LockAsync())
        {
            await File.AppendAllTextAsync(_filePath, message + Environment.NewLine);
        }
    }
}

多个异步任务并发写同一个文件时,也不会出现内容交错。

高级用法

带超时控制的 AsyncLock

csharp 复制代码
public class AsyncLockWithTimeout
{
    private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1);
    
    public async Task<LockResult> TryLockAsync(TimeSpan timeout, CancellationToken cancellationToken = default)
    {
        if (await _semaphore.WaitAsync(timeout, cancellationToken))
        {
            return new LockResult(this, true);
        }
        return new LockResult(this, false);
    }
    
    public class LockResult : IDisposable
    {
        private readonly AsyncLockWithTimeout _lock;
        private readonly bool _acquired;
        
        public bool Acquired => _acquired;
        
        public LockResult(AsyncLockWithTimeout asyncLock, bool acquired)
        {
            _lock = asyncLock;
            _acquired = acquired;
        }
        
        public void Dispose()
        {
            if (_acquired)
            {
                _lock._semaphore.Release();
            }
        }
    }
}

// 使用示例
public async Task<bool> TryProcessWithTimeoutAsync()
{
    using var lockResult = await _lock.TryLockAsync(TimeSpan.FromSeconds(5));
    
    if (lockResult.Acquired)
    {
        // 成功获取锁
        await ProcessDataAsync();
        return true;
    }
    else
    {
        // 获取锁超时
        return false;
    }
}

性能与注意事项

优点

  • 异步友好,不会阻塞线程;

  • 简洁易用;

  • 线程安全。

注意

  • 不适合高频率、极短临界区操作(SemaphoreSlim 有开销);

  • 不要长时间持有锁;

  • 推荐作用于需要保护的异步资源(如数据库、文件、共享状态)。

对比总结

场景 推荐锁类型
同步代码块 lock
异步方法 AsyncLock
控制并发数 SemaphoreSlim
跨进程或跨机器 分布式锁(Redis、SQL、Zookeeper 等)
相关推荐
阿蒙Amon11 小时前
C#每日面试题-Dictionary和Hashtable的区别
java·面试·c#
乐园游梦记11 小时前
工业视觉(尤其是 3D/2.5D 相机场景)中针对不同数据类型、精度、用途设计的保存格式
数码相机·opencv·3d·c#
爱说实话12 小时前
c# 20260113
开发语言·c#
阿蒙Amon12 小时前
C#每日面试题-简述命名空间和程序集
java·面试·c#
HEADKON12 小时前
玛伐凯泰mavacamten基于心脏功能监测的剂量调整可以降低心力衰竭风险
c#
DowneyJoy13 小时前
【Unity通用工具类】列表扩展方法ListExtensions
unity·c#·交互
状元岐13 小时前
C#上位机通信故障排查步骤手l
网络·c#
步步为营DotNet14 小时前
深度探究.NET中WeakReference:灵活内存管理的利器
java·jvm·.net
追逐时光者21 小时前
一个致力于为 C# 程序员提供更佳的编码体验和效率的 Visual Studio 扩展插件
后端·c#·visual studio
SunflowerCoder1 天前
EF Core + PostgreSQL 配置表设计踩坑记录:从 23505 到 ChangeTracker 冲突
数据库·postgresql·c#·efcore