Что такое Semaphore в контексте многопоточности C#?

«Что такое Semaphore в контексте многопоточности C#?» — вопрос из категории Многопоточность, который задают на 47% собеседований C# Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Semaphore — это примитив синхронизации, который ограничивает количество потоков, которые могут одновременно получить доступ к общему ресурсу или пулу ресурсов. В отличие от lock/Monitor, который является мьютексом (пропускает только один поток), семафор может пропускать заданное максимальное количество потоков одновременно.

Основные понятия:

  • Счётчик — внутренний счётчик семафора. При создании задаётся его начальное и максимальное значение.
  • WaitOne() — поток вызывает этот метод, чтобы "войти" в семафор. Если счётчик > 0, он уменьшается на 1, и поток продолжает работу. Если счётчик = 0, поток блокируется до тех пор, пока другой поток не вызовет Release().
  • Release() — поток, закончивший работу с ресурсом, вызывает этот метод. Счётчик увеличивается на 1, и один из ожидающих потоков (если есть) разблокируется.

Пример: Ограничение доступа к пулу из 3-х подключений к БД.

using System.Threading;

public class DatabaseConnectionPool
{
    // Семафор, разрешающий максимум 3 одновременных доступа
    private static Semaphore _pool = new Semaphore(initialCount: 3, maximumCount: 3);

    public void AccessDatabase(int threadId)
    {
        Console.WriteLine($"Поток {threadId} ждёт доступа...");
        _pool.WaitOne(); // Уменьшаем счётчик. Если счётчик = 0 — ждём.

        try
        {
            Console.WriteLine($"Поток {threadId} вошёл в критическую секцию.");
            // Имитация работы с БД
            Thread.Sleep(2000);
        }
        finally
        {
            // Release() ВСЕГДА должен быть в finally-блоке, чтобы гарантировать освобождение семафора даже при исключении.
            _pool.Release(); // Увеличиваем счётчик, разрешая вход другому потоку.
            Console.WriteLine($"Поток {threadId} покинул критическую секцию.");
        }
    }
}
// Запуск 10 потоков, но одновременно работать будут не более 3-х.

Важные варианты и детали:

  1. SemaphoreSlim — легковесная версия для синхронизации внутри одного процесса. Рекомендуется к использованию вместо Semaphore для внутрипроцессных сценариев, так как он эффективнее. Поддерживает асинхронное ожидание (WaitAsync).

    private static SemaphoreSlim _asyncPool = new SemaphoreSlim(3, 3);
    // ...
    await _asyncPool.WaitAsync(); // Асинхронное ожидание без блокировки потока.
    try { /* работа */ } finally { _asyncPool.Release(); }
  2. Именованные семафоры (Semaphore) — могут использоваться для синхронизации между разными процессами (межпроцессное взаимодействие).

    // Процесс 1 и Процесс 2 будут использовать один и тот же системный семафор.
    Semaphore crossProcessSemaphore = new Semaphore(1, 1, "GlobalMyAppSemaphore");

Типичные сценарии использования: ограничение числа одновременных HTTP-запросов, управление доступом к пулу ограниченных ресурсов (подключения, порты), реализация "производитель-потребитель" с ограниченной ёмкостью буфера.