Phase 20 of 30 · Topic 20.3

Asynchronous Synchronization Primitives: `SemaphoreSlim`

1Concept

`SemaphoreSlim` provides asynchronous lock acquisition (`await semaphore.WaitAsync()`) that releases the thread back to the ThreadPool while waiting, making it ideal for async rate limiting and connection throttling.

2Architecture Diagram

3 Concurrent Tasks ──> [ SemaphoreSlim(InitialCount: 2) ]
├── Task 1 ──> Enters (Count = 1)
├── Task 2 ──> Enters (Count = 0)
└── Task 3 ──> Awaits non-blockingly until Task 1/2 calls Release()!

3Code Example

C# 13 & .NET 9
using System;
using System.Threading;
using System.Threading.Tasks;

public class SemaphoreSlimDemo
{
    private static readonly SemaphoreSlim Throttler = new(initialCount: 2, maxCount: 2);

    public static async Task AccessResourceAsync(int id)
    {
        await Throttler.WaitAsync(); // Non-blocking async wait
        try
        {
            Console.WriteLine($"[Slot Granted] Task {id} accessing database...");
            await Task.Delay(20);
        }
        finally
        {
            Throttler.Release();
            Console.WriteLine($"[Slot Released] Task {id} finished.");
        }
    }

    public static async Task Main()
    {
        var t1 = AccessResourceAsync(1);
        var t2 = AccessResourceAsync(2);
        var t3 = AccessResourceAsync(3);

        await Task.WhenAll(t1, t2, t3);
    }
}

4Expected Output

[Slot Granted] Task 1 accessing database...
[Slot Granted] Task 2 accessing database...
[Slot Released] Task 1 finished.
[Slot Granted] Task 3 accessing database...
[Slot Released] Task 2 finished.
[Slot Released] Task 3 finished.

5Key Takeaways

  • Always call `Release()` inside a `finally` block.
  • Use `SemaphoreSlim(1, 1)` as an async mutex (`await semaphore.WaitAsync()`).
  • Standard `lock` cannot be used with `await`.