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 维护)使用。该库提供了生产就绪的实现。

使用方式:

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 来保护临界区:

private readonly object _syncRoot = new object();

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

但是在异步代码中:

public async Task IncrementAsync()
{
    lock (_syncRoot)
    {
        await SomeAsyncOperation(); // ❌ 编译错误
    }
}

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

  • await 会让出线程控制权;

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

  • 这会破坏线程安全。

AsyncLock 的基本思想

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

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

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

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

基本原理

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

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();
        }
    }
}

使用示例

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}");
    }
}

调用示例

var tasks = Enumerable.Range(0, 5).Select(_ => IncrementAsync());
await Task.WhenAll(tasks);

输出将是:

Count: 1
Count: 2
Count: 3
Count: 4
Count: 5

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

与 SemaphoreSlim 的区别

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

改进版:支持 CancellationToken

可以进一步增强:

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

异步文件写入

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

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 等)
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容