Asp.Net Core 中的并发控制:SemaphoreSlim 深入理解与实战

摘要

SemaphoreSlim 是 .NET 中用于限制同时访问某些资源的轻量级信号量(可同步也可异步等待)。本文通过生活类比、核心 API 详解、常见场景和可运行示例,帮助你在 ASP.NET Core 或后台任务中安全地控制并发。

生活化类比:餐厅与停车场

  • 餐厅桌位:想象餐厅只有 5 张桌子(槽位),每当顾客入座时就占用一个桌位;离开时释放一个。SemaphoreSlim 就像这 5 张桌子,限制同时就坐的人数,而不是把餐厅完全锁住(不是互斥)。
  • 停车场:停车场有固定车位数,车辆到来若无空位就需要等待或离开(超时/取消)。SemaphoreSlim 允许多个“访客”并发访问,直到槽用尽。

基本概念

  • initialCount:初始可用槽数(可同时通过的许可数量)。
  • maxCount:最大槽数,Release 不可超过它。
  • SemaphoreSlim 是托管内存中的轻量级实现,性能优于内核级 Semaphore,适合绝大多数场景。
  • 实现了 IDisposable,长期占用时可释放。

常用 API(核心)

  • Wait() / Wait(int milliseconds) — 同步阻塞等待许可。
  • WaitAsync() / WaitAsync(CancellationToken) / WaitAsync(TimeSpan, CancellationToken) — 异步等待(推荐用于异步代码,避免线程阻塞)。
  • Release() — 释放一个许可,必须与成功的 Wait/WaitAsync 对应。
  • CurrentCount — 当前可用许可数(只读)。

异步使用模式(推荐)

始终在 finally 中 Release,以防止因异常/取消而导致许可泄露:

// 示例:异步任务内部获取并释放许可
await semaphore.WaitAsync(ct);
try
{
    // 执行业务逻辑
}
finally
{
    semaphore.Release();
}

超时与取消

  • 使用 CancellationToken 能在等待期间取消 WaitAsync。
  • 可用 WaitAsync(TimeSpan, CancellationToken) 或 Wait(timeout) 实现超时逻辑,注意区分返回值与抛出异常的行为:
    • WaitAsync(TimeSpan) 会返回一个 bool(在 .NET 版本不同表现略有差异),建议捕获超时并按需处理。
    • Wait/Wait(int) 在超时或取消时会返回 false 或抛出 OperationCanceledException(视调用方式而定)。

示例:带超时与取消

var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
if (await semaphore.WaitAsync(TimeSpan.FromSeconds(2), cts.Token))
{
    try
    {
        // 执行
    }
    finally
    {
        semaphore.Release();
    }
}
else
{
    // 处理等待超时
}

常见注意事项(要点)

  • Release 必须与每次成功的 Wait/WaitAsync 对应(通常使用 try/finally)。
  • 不要多次 Release 超出 maxCount,否则会抛出 SemaphoreFullException。
  • SemaphoreSlim 不是互斥锁(Mutex):它允许多个并发访问(值 > 1)。
  • 若长期持有 SemaphoreSlim,出于资源管理可以实现并调用 Dispose()。
  • 在 Web 场景中(ASP.NET Core),尽量避免在请求线程上使用同步 Wait(),推荐使用 WaitAsync()。

与其他同步原语的对比

  • lock / Monitor:用于互斥访问(只能 1 个线程),适合保护共享数据结构。
  • Mutex:内核级,支持跨进程,但性能较低。
  • SemaphoreSlim:限制并发数量、支持异步等待、轻量高效。
  • Semaphore:内核级 Semaphore 支持跨进程,通常不如 SemaphoreSlim 高效(托管场景优先使用 SemaphoreSlim)。

实用场景举例

  • 限制并发请求数(例如每次最多同时发起 5 个外部 API 调用)。
  • 控制并发任务处理以减少对数据库或文件句柄的争用。
  • 后台队列消费者并发数控制。

代码演示(完整可运行 C# Demo)

下面示例演示:先不限制并发,再用 SemaphoreSlim 将并发限制为 3 个;并演示超时/取消的典型使用方式。

using System;
using System.Threading;
using System.Threading.Tasks;

class SemaphoreSlimDemo
{
    // 限制同时执行的任务数量为3
    private static SemaphoreSlim semaphore = new SemaphoreSlim(3, 3);

    static async Task Main(string[] args)
    {
        Console.WriteLine("SemaphoreSlim 演示开始");
        Console.WriteLine("========================\n");

        Console.WriteLine("1. 无限制并发执行:");
        await WithoutSemaphore();

        Console.WriteLine("\n" + new string('-', 50) + "\n");

        Console.WriteLine("2. 使用 SemaphoreSlim 限制并发 (最多3个):");
        await WithSemaphore();

        Console.WriteLine("\n演示结束");
    }

    static async Task WithoutSemaphore()
    {
        var tasks = new Task[6];
        for (int i = 0; i < 6; i++)
        {
            int taskId = i + 1;
            tasks[i] = Task.Run(async () =>
            {
                Console.WriteLine($"任务 {taskId} - 开始执行 (无限制) at {DateTime.Now:HH:mm:ss.fff}");
                await Task.Delay(1000);
                Console.WriteLine($"任务 {taskId} - 执行完成 at {DateTime.Now:HH:mm:ss.fff}");
            });
        }
        await Task.WhenAll(tasks);
    }

    static async Task WithSemaphore()
    {
        var tasks = new Task[6];
        for (int i = 0; i < 6; i++)
        {
            int taskId = i + 1;
            tasks[i] = Task.Run(async () =>
            {
                // 带取消/超时示例:如果等待超过 2 秒则放弃
                using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
                bool acquired = false;
                try
                {
                    acquired = await semaphore.WaitAsync(TimeSpan.FromSeconds(2), cts.Token);
                    if (!acquired)
                    {
                        Console.WriteLine($"任务 {taskId} - 等待许可超时,放弃执行");
                        return;
                    }

                    Console.WriteLine($"任务 {taskId} - 获得许可,开始执行 at {DateTime.Now:HH:mm:ss.fff}");
                    await Task.Delay(1000, cts.Token); // 模拟工作
                    Console.WriteLine($"任务 {taskId} - 执行完成,准备释放许可 at {DateTime.Now:HH:mm:ss.fff}");
                }
                catch (OperationCanceledException)
                {
                    Console.WriteLine($"任务 {taskId} - 被取消");
                }
                finally
                {
                    if (acquired)
                        semaphore.Release();
                }
            });
        }

        await Task.WhenAll(tasks);
    }
}

小结

  • SemaphoreSlim 是控制并发的常用而高效的工具,尤其适合需要限制“并发数量”的场景。
  • 在异步代码中优先使用 WaitAsync,并在 finally 中释放许可;使用 CancellationToken 和超时来提高健壮性。
  • 结合具体场景(外部 API、数据库、文件 IO)合理设置并发上限,既能提高吞吐又能保护下游服务。
posted @ 2025-12-25 15:21  代码沉思者  阅读(14)  评论(0)    收藏  举报