博客 / 詳情

返回

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 等)
user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.