当前位置:首页 > C# > 正文

掌握C#异步锁的递归获取控制(深入理解AsyncLock与递归锁机制)

在现代C#开发中,异步编程已成为处理高并发、高性能应用的标准方式。然而,当多个异步任务需要访问共享资源时,如何安全地进行同步就变得至关重要。本文将带你从零开始,深入理解C#异步锁(特别是支持递归获取的异步锁)的工作原理与实现方式,即使是编程新手也能轻松掌握。

掌握C#异步锁的递归获取控制(深入理解AsyncLock与递归锁机制) C#异步锁 递归锁 AsyncLock 异步编程 第1张

什么是异步锁?

传统的 lock 关键字在C#中用于同步线程,但它在异步方法中使用会导致死锁或阻塞线程池线程,因此不适用于 async/await 场景。

为了解决这个问题,.NET 提供了 SemaphoreSlim,我们可以基于它构建一个支持 await异步锁(AsyncLock)。

为什么需要递归获取?

在某些场景下,一个方法可能在已经持有锁的情况下再次调用自身(直接或间接递归),或者调用另一个也需要相同锁的方法。如果锁不支持递归获取,就会导致死锁。

例如:

async Task ProcessData(){    using (await _asyncLock.LockAsync())    {        // 做一些操作        await SaveData(); // SaveData 也尝试获取同一个锁    }}async Task SaveData(){    using (await _asyncLock.LockAsync())    {        // 保存数据    }}

如果没有递归支持,SaveData() 将永远等待自己释放锁,造成死锁。

实现支持递归的异步锁

下面是一个完整的 RecursiveAsyncLock 实现,它支持同一个执行上下文(如同一个异步流)多次获取锁而不死锁:

using System;using System.Threading;using System.Threading.Tasks;public class RecursiveAsyncLock{    private readonly SemaphoreSlim _semaphore = new(1, 1);    private readonly AsyncLocal<int> _recursionCount = new();    private readonly AsyncLocal<object> _owner = new();    public async Task<Releaser> LockAsync(CancellationToken cancellationToken = default)    {        var currentOwner = _owner.Value;        if (currentOwner != null)        {            // 同一个异步上下文已持有锁,递增计数            _recursionCount.Value++;            return new Releaser(this);        }        // 等待获取锁        await _semaphore.WaitAsync(cancellationToken);        _owner.Value = new object();        _recursionCount.Value = 1;        return new Releaser(this);    }    private void Release()    {        if (_owner.Value == null)            throw new InvalidOperationException("Lock was not held.");        var count = _recursionCount.Value;        if (count > 1)        {            _recursionCount.Value = count - 1;        }        else        {            _recursionCount.Value = 0;            _owner.Value = null;            _semaphore.Release();        }    }    public struct Releaser : IDisposable    {        private readonly RecursiveAsyncLock _lock;        public Releaser(RecursiveAsyncLock @lock)        {            _lock = @lock;        }        public void Dispose()        {            _lock?.Release();        }    }}

关键点解析

  • AsyncLocal<T>:用于在异步流中保持上下文状态。即使在 await 之后,也能识别是否是“同一个调用链”。
  • 递归计数:每次进入锁时计数+1,退出时-1,只有计数归零才真正释放底层信号量。
  • Releaser 结构体:通过 IDisposable 实现 using 语法糖,确保锁被正确释放。

使用示例

class DataService{    private readonly RecursiveAsyncLock _lock = new();    public async Task Process()    {        using (await _lock.LockAsync())        {            Console.WriteLine("Processing...");            await Save(); // 递归获取锁        }    }    private async Task Save()    {        using (await _lock.LockAsync())        {            Console.WriteLine("Saving...");            await Task.Delay(100);        }    }}

运行上述代码不会死锁,因为 RecursiveAsyncLock 允许同一个异步流多次进入。

注意事项

  • 不要在不同线程间共享 RecursiveAsyncLock 的实例用于跨线程递归(它基于 AsyncLocal,仅限单个异步流)。
  • 避免长时间持有异步锁,以免阻塞其他任务。
  • 如果不需要递归,可使用更简单的非递归 AsyncLock(性能略优)。

总结

通过本文,你已经掌握了如何在C#中实现和使用支持递归获取的异步锁。这种模式在复杂的异步业务逻辑中非常有用,能有效避免死锁,同时保证线程安全。记住,合理使用 AsyncLock、理解 递归锁 的适用场景,是编写健壮异步代码的关键。

关键词回顾:C#异步锁、递归锁、AsyncLock、异步编程