在C#的异步编程场景里,传统的lock关键字只能作用于同步代码块,无法在包含await的异步方法中使用,否则会直接编译报错。这时候SemaphoreSlim就成为了实现异步锁的最佳选择,它支持异步等待获取锁,完美适配异步编程模型。

SemaphoreSlim基础介绍
SemaphoreSlim是.NET提供的轻量级信号量实现,相比传统的Semaphore类,它没有使用Windows内核对象,性能更高,更适合进程内的线程同步场景。它的核心作用是限制同时访问某一资源或资源池的线程数量,当初始计数设置为1时,就可以实现类似互斥锁的效果,也就是我们常说的异步锁。
SemaphoreSlim常用的核心方法如下:
WaitAsync():异步等待获取信号量,可设置超时时间,不会阻塞当前线程Release():释放信号量,增加信号量的计数,唤醒等待的线程AvailableWaitHandle:获取用于等待信号量的等待句柄,一般异步场景很少使用
异步锁的基本使用步骤
使用SemaphoreSlim实现异步加锁的流程非常简单,主要分为三步:初始化信号量、异步等待获取锁、释放锁。需要注意释放锁的操作一定要放在try-finally块中,避免获取锁之后因为异常导致锁无法释放,造成死锁。
基础使用示例
下面是一个简单的异步加锁示例,模拟多个异步任务同时访问共享资源的场景:
using System;
using System.Threading;
using System.Threading.Tasks;
class Program
{
// 初始化SemaphoreSlim,初始计数为1,最大计数也为1,实现互斥锁效果
private static SemaphoreSlim _asyncLock = new SemaphoreSlim(1, 1);
// 共享资源
private static int _sharedCount = 0;
static async Task Main(string[] args)
{
// 创建10个异步任务同时执行
var tasks = new Task[10];
for (int i = 0; i < 10; i++)
{
tasks[i] = AccessSharedResourceAsync(i);
}
await Task.WhenAll(tasks);
Console.WriteLine($"最终共享资源值:{_sharedCount}");
}
static async Task AccessSharedResourceAsync(int taskId)
{
Console.WriteLine($"任务{taskId}开始等待获取锁");
// 异步等待获取锁
await _asyncLock.WaitAsync();
try
{
Console.WriteLine($"任务{taskId}获取到锁,开始操作共享资源");
// 模拟异步操作
await Task.Delay(100);
// 操作共享资源
int temp = _sharedCount;
await Task.Delay(50);
_sharedCount = temp + 1;
Console.WriteLine($"任务{taskId}操作完成,当前共享资源值:{_sharedCount}");
}
finally
{
// 释放锁,确保无论是否出现异常都会执行
_asyncLock.Release();
Console.WriteLine($"任务{taskId}释放锁");
}
}
}
运行上述代码可以看到,所有任务都是依次获取锁、操作资源、释放锁,不会出现多个任务同时修改共享资源的情况,最终_sharedCount的值会是10,符合预期。
带超时的异步锁使用
在实际场景中,我们可能不希望任务无限等待获取锁,这时候可以给WaitAsync方法设置超时时间,如果超过指定时间没有获取到锁,就执行其他逻辑。
using System;
using System.Threading;
using System.Threading.Tasks;
class TimeoutExample
{
private static SemaphoreSlim _asyncLock = new SemaphoreSlim(1, 1);
static async Task Main(string[] args)
{
// 先启动一个任务长期持有锁
var holdLockTask = HoldLockLongTimeAsync();
await Task.Delay(100);
// 另一个任务尝试获取锁,设置500毫秒超时
bool isLockAcquired = await _asyncLock.WaitAsync(500);
if (isLockAcquired)
{
try
{
Console.WriteLine("成功获取到锁,执行操作");
}
finally
{
_asyncLock.Release();
}
}
else
{
Console.WriteLine("获取锁超时,执行超时逻辑");
}
await holdLockTask;
}
static async Task HoldLockLongTimeAsync()
{
await _asyncLock.WaitAsync();
try
{
Console.WriteLine("长期持有锁的任务开始执行");
await Task.Delay(2000);
Console.WriteLine("长期持有锁的任务执行完成");
}
finally
{
_asyncLock.Release();
}
}
}
上述代码中,第二个任务尝试获取锁时设置了500毫秒的超时,而第一个任务会持有锁2秒,因此第二个任务会获取锁失败,执行超时逻辑。
SemaphoreSlim使用注意事项
- 一定要保证
Release的调用次数和WaitAsync的调用次数匹配,否则会导致信号量计数异常,甚至引发异常。如果不确定,可以在Release时指定释放的数量,默认是释放1个。 - SemaphoreSlim实现了
IDisposable接口,如果确定不再使用,可以调用Dispose方法释放资源,不过一般进程内的同步场景不需要手动释放,进程结束会自动回收。 - 不要尝试在SemaphoreSlim的等待过程中使用
lock关键字,两者属于不同的同步机制,混合使用很容易引发死锁。 - 如果需要在多个进程之间同步资源,SemaphoreSlim无法满足需求,需要使用传统的Semaphore类,因为它支持命名系统信号量,可以实现跨进程同步。
和普通lock的对比
为了更清晰地了解SemaphoreSlim的适用场景,我们可以把它和传统的lock关键字做对比:
| 对比项 | lock关键字 | SemaphoreSlim(初始计数1) |
|---|---|---|
| 适用场景 | 同步代码块,无await操作 | 异步代码块,包含await操作 |
| 等待方式 | 阻塞当前线程 | 异步等待,不阻塞线程 |
| 超时支持 | 不支持 | 支持WaitAsync超时 |
| 跨进程支持 | 不支持 | 不支持 |
| 性能 | 很高 | 较高,略低于lock |
通过对比可以看出,在异步场景下优先选择SemaphoreSlim实现加锁,同步场景下优先使用lock关键字,这样能获得更好的性能。
C#SemaphoreSlim异步锁异步编程线程同步修改时间:2026-06-27 19:51:33