JavaRush /Курсы /C# SELF /Семафоры: Semaphore...

Семафоры: Semaphore и SemaphoreSlim

C# SELF
56 уровень , 3 лекция
Открыта

1. Введение

Mutex и lock — это как бариста, который обслуживает одного клиента за раз. Но что, если у нас не одинокая кофеварка, а целых три — и одновременно можно готовить три чашки кофе?

Например, у вас есть кафе с тремя кофемашинами. Клиенты (потоки) подходят, занимают свободную машину, делают кофе и уходят. Если все три машины заняты, остальные ждут, пока освободится хоть одна.

Вопрос: Как сделать так, чтобы одновременно за кофемашинами работали не более трёх клиентов, а остальные ждали своей очереди?
Ответ: использовать семафор!

Что такое семафор?

Семафор — классический инструмент синхронизации. Если lock/Mutex контролируют "один зашёл — остальные ждут", то семафор говорит: "допускаю N одновременно!".

Семафоры были предложены Эдсгером Дейкстрой в 1965 году. Название пришло из морской сигнализации: как флажки передавали доступную информацию, так и семафор в коде сообщает потокам — можно входить или нужно подождать.

Сценарии использования

  • Ограничение числа потоков, одновременно работающих с ресурсом.
  • Лимит одновременных подключений к БД, числа параллельных запросов, тяжёлых задач.

2. Обзор классов: Semaphore и SemaphoreSlim

Semaphore

  • Тяжёлый класс, использует объекты ядра ОС (kernel objects).
  • Поддерживает синхронизацию между потоками разных процессов.
  • Можно задавать имя и разделять между процессами.

SemaphoreSlim

  • Облегчённая версия, работает только внутри одного процесса.
  • Быстрее и экономичнее по ресурсам.
  • Почти всегда предпочтителен, если межпроцессная синхронизация не нужна.

Аналогия: походный рюкзак (SemaphoreSlim) против большого чемодана (Semaphore). Путешествуете налегке — берите рюкзак.

Сравнительная таблица

Класс Межпроцессный Быстродействие Рекомендуется
Semaphore
Да Медленнее Когда нужна синхронизация между процессами
SemaphoreSlim
Нет Быстрее В 99% случаев, внутри одного процесса

Основные методы и свойства семафора

Основные параметры

  • InitialCount — начальное количество разрешений.
  • MaxCount — максимум одновременно выданных разрешений.

Ключевые методы

  • Wait() или WaitAsync() — запросить доступ (занять разрешение).
  • Release() — освободить разрешение.

Как это работает
Если при вызове Wait() разрешений нет, поток блокируется и ждёт, пока кто-то вызовет Release(). После освобождения одно из ожидающих продолжит выполнение.

3. Первый практический пример

Добавим в консольное приложение "парковку" на 3 места и попробуем запустить 10 потоков.

using System;
using System.Threading;

class Program
{
    // Семaфор с 3 разрешениями (3 парковочных места)
    static SemaphoreSlim parking = new SemaphoreSlim(3);

    static void Main()
    {
        for (int i = 1; i <= 10; i++)
        {
            int carNumber = i;
            new Thread(() =>
            {
                Console.WriteLine($"Машина #{carNumber} пытается припарковаться...");
                parking.Wait(); // Ожидает свободное место
                Console.WriteLine($"Машина #{carNumber} заехала на парковку!");
                Thread.Sleep(2000); // Стоим на парковке 2 секунды
                Console.WriteLine($"Машина #{carNumber} выезжает с парковки.");
                parking.Release(); // Освобождаем место
            }).Start();
        }
    }
}
  • Одновременно "припаркуются" только три машины.
  • Остальные будут ждать освобождения места.
  • Вывод перемешан — это нормально для многопоточности.

4. Семафор как ограничитель нагрузки

Ограничим число одновременно выполняемых тяжёлых задач (например, загрузок) до 5.

static SemaphoreSlim semaphore = new SemaphoreSlim(5); // максимум 5 одновременных загрузок

static void DownloadFile(int fileId)
{
    semaphore.Wait();
    try
    {
        Console.WriteLine($"--> Начинаю загрузку файла {fileId}");
        Thread.Sleep(1000 + fileId * 100); // Загружаем (имитация)
        Console.WriteLine($"<-- Файл {fileId} загружен");
    }
    finally
    {
        semaphore.Release();
    }
}

static void Main()
{
    for (int i = 1; i <= 12; i++)
    {
        int localId = i;
        new Thread(() => DownloadFile(localId)).Start();
    }
}

Важный момент: Wait() размещаем до блока try, а Release() — в finally. Так разрешение точно освободится даже при исключении.

5. Wait(int millisecondsTimeout) и асинхронные методы

Можно ждать только ограниченное время:

if (semaphore.Wait(500))
{
    // Получилось занять разрешение за полсекунды!
}
else
{
    // За 500 мс не дождались — прервались
}

В современных приложениях (например, ASP.NET) используйте асинхронный вариант: await semaphore.WaitAsync(). Это не блокирует поток выполнения, пока ожидается разрешение.
Примечание: в асинхронном коде используйте именно SemaphoreSlim и его WaitAsync, иначе можно получить неожиданные deadlock-и.

6. Примеры неправильной и правильной работы

Частая ошибка — забыть вызвать Release(): разрешения "утекают", и всё встаёт.

Плохо

static void SomeWork()
{
    semaphore.Wait();
    // ... обработка, а Release забыли!
}

Хорошо

static void SomeWork()
{
    semaphore.Wait();
    try
    {
        // обработка
    }
    finally
    {
        semaphore.Release();
    }
}

Асинхронный вариант

static async Task SomeAsyncWork()
{
    await semaphore.WaitAsync();
    try
    {
        // асинхронная обработка
    }
    finally
    {
        semaphore.Release();
    }
}

7. Внутреннее устройство семафора (объяснение "на пальцах")

Семафор — это счётчик. Wait() уменьшает его на 1. Если он был > 0 — поток проходит; если 0 — поток ждёт. Release() увеличивает счётчик и будит ожидающих.


+-------------------------------+
| Семафор (счётчик = 3)         |
+-------------------------------+
|  [ ]  [ ]  [ ]                | <--- Разрешения
+----+----+----+----------------+
     |    |    |
   Поток Поток Поток

8. Полезные нюансы

Различие с другими примитивами

  • lock / Monitor / Mutex — пускают только один поток (эксклюзивный доступ).
  • Semaphore/SemaphoreSlim — пускают ограниченно N потоков одновременно.

Семафор не привязан к "владельцу": разрешение может освобождать любой поток. Это особенность, а не баг.

Применение в реальной жизни

  • Лимит параллельных подключений к сервису или БД.
  • Пул: не более N потоков у ресурса.
  • Ограничение одновременно обрабатываемых веб‑запросов.
  • Лимит чтения/записи для защиты от перегрузки.
  • Ограничение вызовов внешнего API.

Пример ошибки (Release больше, чем Wait)

var semaphore = new SemaphoreSlim(2);
semaphore.Release(); // Ошибка! Счётчик стал 3, превышая MaxCount — будет выброшено SemaphoreFullException.

Здесь будет SemaphoreFullException: счётчик превысил максимум.

Отличия Semaphore и SemaphoreSlim

  • SemaphoreSlim — внутри процесса, быстрее и проще (используйте почти всегда).
  • Semaphore — нужен для межпроцессной синхронизации (редкий сценарий).

Зачем знать про семафоры?

Классический вопрос на интервью: "Как ограничить количество потоков, работающих с ресурсом?" — правильный ответ: семафор.

  • lock — 1 поток.
  • Semaphore/SemaphoreSlimN потоков.

9. Типичные ошибки и особенности использования семафоров

Ошибка №1: забывают вызвать Release(). Если поток занял разрешение (Wait() или WaitAsync()), но не освободил его, остальные будут ждать бесконечно — приложение "замрёт".

Ошибка №2: вызывают Release() больше раз, чем было Wait(). Появляются "лишние" разрешения. Для Semaphore это приведёт к SemaphoreFullException и сломанной логике доступа.

Ошибка №3: смешивают разные механизмы синхронизации. В одном месте — lock, в другом — семафор на один и тот же ресурс. Это повышает риск взаимных блокировок (deadlock).

Ошибка №4: используют Semaphore в асинхронном коде. Классический семафор не дружит с async/await. Для асинхронных сценариев используйте SemaphoreSlim и WaitAsync().

Ошибка №5: неверно задают initialCount и maxCount. При неправильном выборе значений ограничение можно обойти, и к ресурсу пройдут больше потоков, чем задумано.

2
Задача
C# SELF, 56 уровень, 3 лекция
Недоступна
Имитация работы парковки с потоками
Имитация работы парковки с потоками
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ