簡介
AsyncLock 是一種自定義的異步互斥鎖(Mutex Lock),專為異步編程場景設計,用於在 async/await 方法中實現線程安全的互斥訪問。它彌補了 .NET 中傳統 lock 語句(基於 Monitor)的不足,因為 lock 是同步阻塞的,在異步環境中會阻塞線程池線程,導致性能下降或死鎖風險。
- 核心原理:
AsyncLock通常基於SemaphoreSlim(1, 1)實現,允許異步等待鎖的獲取,而不阻塞當前線程。等待的任務會被掛起(suspend),釋放線程池資源,支持CancellationToken取消操作。 - 來源:
.NET標準庫中沒有內置AsyncLock,通常通過NuGet包Nito.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 API或TCP處理)中。 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 等) |