AsyncLock 是 EasilyNET.Core 中用于实现异步互斥的核心原语。它的设计目标是在高并发场景下提供极低的内存分配和高性能,同时保证严格的先进先出(FIFO)顺序。
-
快慢路径分离 (Fast/Slow Path):
- 绝大多数无竞争场景下,通过
Interlocked原子操作即可完成锁定,零内存分配(除了 Struct 包装)。 - 仅在发生竞争时才实例化等待节点(Waiter)。
- 绝大多数无竞争场景下,通过
-
所有权移交 (Handoff Semantics):
- 释放锁时,直接将所有权移交给队列中的下一个等待者,而不是将锁重置为“空闲”。
- 优势: 防止了“抢占(Barging)”,即新来的请求抢走了刚刚释放的锁,导致排队者饥饿。
-
O(1) 取消机制:
- 使用
LinkedList存储等待者,支持$O(1)$ 复杂度的节点移除,这对于支持CancellationToken至关重要。
- 使用
flowchart TD
Start([LockAsync]) --> CheckState{CAS _state 0 -> 1}
%% Fast Path
CheckState -- Success --> AcquiredFast[Acquired 'Fast Path']
%% Slow Path
CheckState -- Fail --> InitWaiter[Create Waiter]
InitWaiter --> RegCancel[Register CancellationToken]
RegCancel --> LockSync[Lock _sync]
LockSync --> DoubleCheck{CAS _state 0 -> 1}
DoubleCheck -- Success --> CleanWaiter[Dispose Waiter] --> AcquiredSlow[Acquired 'Double Check']
DoubleCheck -- Fail --> Enqueue[Add Waiter to LinkedList]
Enqueue --> ReturnTask[Return Waiter Task]
ReturnTask --> AwaitTask(Wait for Wakeup)
flowchart TD
Start([ReleaseInternal]) --> LoopStart{Loop}
LoopStart --> LockSync[Lock _sync]
LockSync --> CheckWaiters{Waiters > 0?}
%% No Waiters - Unlock
CheckWaiters -- No --> ResetState[Set _state = 0]
ResetState --> UnlockReturn([Lock Released])
%% Have Waiters - Handoff
CheckWaiters -- Yes --> Dequeue[Remove First Waiter]
Dequeue --> KeepState[Keep _state = 1 'Transfer Ownership']
KeepState --> UnlockSync[Unlock _sync]
UnlockSync --> TryWake{TrySetResult?}
TryWake -- Success --> WakeDone([Done])
TryWake -- Fail (Cancelled) --> LoopStart
// 0 = 空闲, 1 = 占用
private int _state;
// 快路径尝试
if (Interlocked.CompareExchange(ref _state, 1, 0) == 0)
{
// 获取成功
}private sealed class Waiter
{
// 使用 RunContinuationsAsynchronously 防止栈溢出
internal readonly TaskCompletionSource<Release> Tcs = new(TaskCreationOptions.RunContinuationsAsynchronously);
internal LinkedListNode<Waiter>? Node;
// ...
}取消注册使用了 UnsafeRegister,避免捕获 ExecutionContext,减少开销。
waiter.CancellationRegistration = cancellationToken.UnsafeRegister(..., waiter);AsyncLock 旨在替代 Python/C# 中常见的 SemaphoreSlim(1, 1) 模式,提供更安全、更易用的 API。
最常见的模式是使用 using 语句块,确保锁在作用域结束时自动释放:
private readonly AsyncLock _mutex = new();
public async Task ProcessDataAsync()
{
// 获取锁
using (await _mutex.LockAsync())
{
// 临界区代码:同一时间只有一个线程能执行此处
await DoSomethingCriticalAsync();
}
// 锁在此处自动释放
}防止因死锁或长时间等待导致的系统卡死:
public async Task ProcessWithTimeoutAsync()
{
// 尝试在 3 秒内获取锁
var result = await _mutex.WaitAsync(TimeSpan.FromSeconds(3));
if (result.Acquired)
{
using (result.Releaser) // 务必释放等待结果中的 Releaser
{
await DoWorkAsync();
}
}
else
{
// 获取锁超时处理逻辑
Console.WriteLine("获取锁超时!");
}
}完全支持 CancellationToken,适合 Web API 请求处理:
public async Task ProcessRequestAsync(CancellationToken token)
{
try
{
// 如果 token 被取消,这里会抛出 OperationCanceledException
using (await _mutex.LockAsync(token))
{
await DoWorkAsync(token);
}
}
catch (OperationCanceledException)
{
// 处理取消逻辑
}
}虽然推荐在异步代码中使用,但也支持同步尝试获取(非阻塞):
public void TryUpdateData()
{
// 尝试立即获取锁,不等待
if (_mutex.TryLock(out var releaser))
{
using (releaser)
{
// 只有获取到锁才会执行
UpdateData();
}
}
else
{
Console.WriteLine("当前正忙,请稍后再试");
}
}-
非可重入 (Non-Reentrant):
- 与
Monitor(lock) 不同,AsyncLock是不可重入的。 - 错误示例:
using (await _mutex.LockAsync()) { using (await _mutex.LockAsync()) // 死锁!永远在等待自己释放 { ... } }
- 与
-
结构体释放 (struct Dispose):
LockAsync返回的是一个struct Release,分配在栈上,零 GC 开销。- 务必使用
using或try-finally确保Dispose被调用,否则锁将永远不会释放。
-
性能极佳:
- 在无竞争情况下,
AsyncLock使用Interlocked操作,性能远超SemaphoreSlim。 - 在高并发竞争下,基于 FIFO 队列调度,保证公平性,避免线程饥饿。
- 在无竞争情况下,