目录
在.NET Core API 开发中,并发控制是保证系统稳定性和数据一致性的关键环节。当多个请求同时访问共享资源时,若不加以控制,很容易引发数据错乱、性能下降等问题。SemaphoreSlim作为.NET 框架中轻量级的信号量实现,为我们提供了简单而高效的并发控制方案。本文将详细介绍 SemaphoreSlim 的工作原理、使用方法以及在 Web 开发中的常见应用场景。
什么是 SemaphoreSlim
SemaphoreSlim 是.NET Framework 4.0 引入的轻量级信号量类,位于System.Threading命名空间下。它专为短期等待场景设计,相比传统的Semaphore类,具有更低的系统开销和更高的性能。
信号量的核心作用是限制同时访问某个资源的并发数量。可以将其理解为一个带计数器的门控机制:当计数器大于 0 时,允许线程进入并将计数器减 1;当计数器为 0 时,后续线程必须等待直到有线程释放资源(计数器加 1)。
与传统 Semaphore 相比,SemaphoreSlim 的优势在于:
- 不依赖操作系统内核对象,减少了用户态与内核态之间的切换开销
- 支持异步等待(WaitAsync方法),非常适合异步 API 开发
- 内存占用更小,创建和销毁的成本更低
- 支持取消令牌(CancellationToken),便于实现超时控制和操作取消
SemaphoreSlim 的核心方法
SemaphoreSlim 提供了一组简洁而强大的方法来实现并发控制,掌握这些方法是正确使用 SemaphoreSlim 的基础:
构造函数
// 初始化一个可同时允许maxCount个线程访问的信号量
public SemaphoreSlim(int initialCount);
public SemaphoreSlim(int initialCount, int maxCount);
- initialCount:信号量的初始计数,即初始允许的并发数量
- maxCount:信号量的最大计数,规定了允许的最大并发数量
等待方法
// 同步等待,直到信号量可用
public void Wait();
public bool Wait(int millisecondsTimeout);
public bool Wait(TimeSpan timeout);
public void Wait(CancellationToken cancellationToken);
// 异步等待,适合异步方法中使用
public Task WaitAsync();
public Task<bool> WaitAsync(int millisecondsTimeout);
public Task<bool> WaitAsync(TimeSpan timeout);
public Task WaitAsync(CancellationToken cancellationToken);
等待方法的作用是申请访问权限:当调用这些方法时,线程会尝试获取信号量。如果当前计数大于 0,计数减 1 并立即返回;否则,线程会进入等待状态,直到有其他线程释放信号量或等待超时 / 被取消。
释放方法
// 释放信号量,增加计数
public void Release();
public int Release(int releaseCount);
释放方法用于归还访问权限:当线程完成对共享资源的访问后,应调用Release方法将信号量计数加 1,允许其他等待的线程获取访问权限。Release(int releaseCount)可以一次释放多个计数,但释放的总数不能超过maxCount。
基本使用模式
使用 SemaphoreSlim 的核心原则是 **"先等待,后访问,最后释放"**,通常遵循以下模式:
同步使用模式
// 创建一个最多允许3个并发访问的信号量
private static readonly SemaphoreSlim _semaphore = new SemaphoreSlim(3);
public void AccessResource()
{
try
{
// 等待获取信号量
_semaphore.Wait();
// 访问共享资源的逻辑
UseSharedResource();
}
finally
{
// 释放信号量,必须放在finally中确保一定会执行
_semaphore.Release();
}
}
异步使用模式(推荐在 API 中使用)
private static readonly SemaphoreSlim _semaphore = new SemaphoreSlim(3);
public async Task AccessResourceAsync()
{
try
{
// 异步等待获取信号量
await _semaphore.WaitAsync();
// 异步访问共享资源的逻辑
await UseSharedResourceAsync();
}
finally
{
// 释放信号量
_semaphore.Release();
}
}
在 Web 开发中的常见用途
在.NET Core API 开发中,SemaphoreSlim 有多种实用场景,尤其适合处理那些需要限制并发量的操作:
1. 限制 API 接口的并发请求数
当 API 接口调用涉及到资源密集型操作(如复杂计算、大数据处理)时,无限制的并发可能导致服务器资源耗尽。使用 SemaphoreSlim 可以限制同时处理的请求数量:
[ApiController]
[Route("api/[controller]")]
public class DataProcessorController : ControllerBase
{
// 限制最多5个并发请求
private static readonly SemaphoreSlim _processorSemaphore = new SemaphoreSlim(5);
[HttpPost("process")]
public async Task<IActionResult> ProcessData([FromBody] DataRequest request)
{
try
{
// 等待获取处理权限,设置5秒超时
if (!await _processorSemaphore.WaitAsync(5000))
{
return StatusCode(StatusCodes.Status429TooManyRequests,
"当前请求过多,请稍后再试");
}
// 处理数据
var result = await DataProcessor.ProcessAsync(request);
return Ok(result);
}
finally
{
_processorSemaphore.Release();
}
}
}
2. 保护共享资源的并发访问
当多个请求需要访问同一个共享资源(如文件、非线程安全的服务实例)时,SemaphoreSlim 可以确保资源操作的原子性:
public class FileService
{
private readonly SemaphoreSlim _fileAccessSemaphore = new SemaphoreSlim(1);
private readonly string _filePath = "data.json";
public async Task UpdateFileAsync(string content)
{
await _fileAccessSemaphore.WaitAsync();
try
{
// 读取当前内容
var currentContent = await File.ReadAllTextAsync(_filePath);
// 处理内容
var newContent = ProcessContent(currentContent, content);
// 写入新内容
await File.WriteAllTextAsync(_filePath, newContent);
}
finally
{
_fileAccessSemaphore.Release();
}
}
}
3. 控制外部服务的调用频率
调用第三方 API 时,通常会有频率限制(Rate Limiting)。使用 SemaphoreSlim 可以控制并发调用数量,避免触发限制:
public class ExternalApiClient
{
private readonly HttpClient _httpClient;
// 根据第三方API的限制设置并发数
private readonly SemaphoreSlim _apiSemaphore = new SemaphoreSlim(10);
private readonly int _apiTimeoutMs = 3000;
public ExternalApiClient(HttpClient httpClient)
{
_httpClient = httpClient;
}
public async Task<TResult> CallApiAsync<TResult>(string endpoint)
{
try
{
// 等待获取调用权限
if (!await _apiSemaphore.WaitAsync(_apiTimeoutMs))
{
throw new TimeoutException("等待调用第三方API超时");
}
// 调用外部API
var response = await _httpClient.GetAsync(endpoint);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<TResult>();
}
finally
{
_apiSemaphore.Release();
}
}
}
4. 实现分布式锁的本地补充
在分布式系统中,虽然需要分布式锁(如 Redis 锁)来跨实例同步,但结合 SemaphoreSlim 可以减少分布式锁的竞争:
public class DistributedTaskService
{
private readonly IDistributedLock _distributedLock;
// 每个实例最多处理2个任务,减少分布式锁竞争
private readonly SemaphoreSlim _localSemaphore = new SemaphoreSlim(2);
public async Task ExecuteTaskAsync(string taskId)
{
// 先获取本地信号量,减少分布式锁的竞争
await _localSemaphore.WaitAsync();
try
{
// 再获取分布式锁
using (var lockResult = await _distributedLock.AcquireLockAsync(taskId))
{
if (lockResult.Success)
{
await ProcessTaskAsync(taskId);
}
}
}
finally
{
_localSemaphore.Release();
}
}
}
注意事项与最佳实践
虽然 SemaphoreSlim 使用简单,但在实际应用中仍需注意以下几点,以避免常见问题:
1. 确保正确释放信号量
最常见的错误是忘记释放信号量或在异常情况下未能释放,这会导致信号量计数永远无法恢复,最终所有请求都陷入无限等待。因此,务必将Release调用放在finally块中,确保无论是否发生异常都会执行。
2. 合理设置信号量的生命周期
在 Web 应用中,SemaphoreSlim 通常应声明为static或使用单例模式,以确保它在应用生命周期内保持状态。如果每次请求都创建新的 SemaphoreSlim 实例,将失去并发控制的作用。
// 正确:静态字段,应用域内共享
private static readonly SemaphoreSlim _semaphore = new SemaphoreSlim(5);
// 错误:每次实例化都会创建新的信号量
public class MyController : ControllerBase
{
private readonly SemaphoreSlim _badSemaphore = new SemaphoreSlim(5);
// ...
}
3. 避免过度限制并发
信号量的作用是 "限制" 而非 "禁止" 并发,设置过小的并发数会导致系统吞吐量下降。应根据实际负载测试结果,结合服务器资源(CPU、内存、IO 等)合理设置最大并发数。
4. 注意异步操作中的取消机制
在异步场景中,建议使用带CancellationToken的WaitAsync重载,以便在请求被取消(如客户端断开连接)时能够及时释放资源:
// 结合HTTP请求的取消令牌
public async Task<IActionResult> MyAction()
{
try
{
// 使用RequestAborted令牌
await _semaphore.WaitAsync(ControllerContext.HttpContext.RequestAborted);
// ...
}
finally
{
_semaphore.Release();
}
}
5. 警惕死锁风险
当使用多个信号量时,可能会出现死锁:线程 A 持有信号量 1 并等待信号量 2,而线程 B 持有信号量 2 并等待信号量 1。避免这种情况的方法是:
- 尽量使用单个信号量解决问题
- 如果必须使用多个信号量,确保所有线程按相同的顺序获取它们
6. 结合限流中间件使用
对于 API 级别的全局限流,SemaphoreSlim 可以与ASP.NET Core 的限流中间件配合使用,但注意不要重复限流导致性能问题。
7. 多实例部署的局限性
需要注意的是,SemaphoreSlim 是进程内的并发控制机制,无法跨多个应用实例同步。在多服务器、多容器部署的场景中,还需要结合分布式锁(如基于 Redis 或 ZooKeeper 的实现)才能实现全局的并发控制。
总结
SemaphoreSlim 是.NET Core API 开发中处理并发控制的得力工具,它轻量高效,支持异步操作,非常适合 Web 环境下的短期等待场景。通过合理使用 SemaphoreSlim,我们可以有效限制资源访问的并发数量,保护共享资源,控制外部服务调用频率,从而提高系统的稳定性和可靠性。
使用 SemaphoreSlim 的核心在于理解 "获取 - 使用 - 释放" 的模式,并始终牢记在finally块中释放信号量。同时,也要认识到它在多实例部署中的局限性,必要时结合分布式方案进行补充。掌握这些知识,将帮助你在实际开发中更好地应对并发挑战,构建更健壮的 API 服务。