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 等)
相关推荐
六bring个六14 小时前
文件压缩处理(一)
开发语言·c#
superman超哥20 小时前
仓颉语言中字符串常用方法的深度剖析与工程实践
开发语言·后端·python·c#·仓颉
bugcome_com20 小时前
C# 中 ref 与 out 参数传递:核心区别与实战解析
c#
用户44884667106020 小时前
.NET 进阶 —— 深入理解线程(3)ThreadPool 与 Task 入门:从手动线程到池化任务的升级
c#·.net
CreasyChan21 小时前
unity四元数 - “处理旋转的大师”
unity·c#·游戏引擎
wuguan_21 小时前
C#索引器
c#·索引器
聪明努力的积极向上21 小时前
【设计】分批查询数据通用方法(基于接口 + 泛型 + 定点复制)
开发语言·设计模式·c#
张人玉1 天前
C# WPF 折线图制作(可以连接数据库)
数据库·c#·wpf·sugar