Что такое Semaphore в контексте многопоточности 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-запросов, управление доступом к пулу ограниченных ресурсов (подключения, порты), реализация "производитель-потребитель" с ограниченной ёмкостью буфера.

Ответ 18+ 🔞

Ну ты представляешь, семафор — это такая штука, которая как швейцар в переполненный клуб пускает. Не одного, как обычный лок, а сразу пачку ребят, но не больше, чем стульев внутри. То есть, грубо говоря, это ограничитель потока потоков, если по-простому.

Суть в двух словах:

  • Счётчик — у семафора внутри табло, как в бане: «Свободно мест: N». При создании задаёшь, сколько всего мест и сколько свободно сразу.
  • WaitOne() — это когда поток подходит к турникету и говорит: «Пустите». Если место есть — счётчик уменьшается, и он проходит. Если мест ноль — стоит и ждёт, пока кто-то не выйдет.
  • Release() — это когда поток, наконец-то, отъел своё и вываливается из клуба. Счётчик увеличивается, и следующий ждущий может зайти.

Пример: У нас есть три стула в баре для подключений к базе, а желающих — десять.

using System.Threading;

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

    public void AccessDatabase(int threadId)
    {
        Console.WriteLine($"Поток {threadId} тусуется у входа и курит...");
        _pool.WaitOne(); // Пытается пройти. Если мест нет — зависнет тут.

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

Важные нюансы, чтобы не обосраться:

  1. SemaphoreSlim — это как облегчённая версия семафора, для своих, внутри одного приложения. Он быстрее и современнее. А главное — умеет в асинхронность без блокировок, что сейчас вообще святое.

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

    // Создаётся системный семафор, видимый всем. Если имя совпадёт — процессы будут использовать один и тот же.
    Semaphore crossProcessSemaphore = new Semaphore(1, 1, "Global\MyAppSemaphore");

Где это обычно применяют: чтобы не отправить сто пятьсот HTTP-запросов одновременно и не положить сервер, чтобы ограничить подключения к базе, или сделать очередь с ограниченной вместимостью, где производители не заваливают потребителей работой под завязку. В общем, полезная хуйня, когда нужно впустить, но не всех сразу.