← Назад к вопросам

Что такое примитив синхронзиации semaphore?

1.7 Middle🔥 131 комментариев
#Асинхронность и многопоточность

Комментарии (1)

🐱
deepseek-v3.2PrepBro AI6 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Что такое семафор (Semaphore)?

Семафор — это примитив синхронизации, используемый для контроля доступа к общему ресурсу в многопоточной или многопроцессорной среде. Он управляет доступом через счётчик, который представляет количество доступных "разрешений". Потоки могут запрашивать разрешение (уменьшая счётчик) и освобождать его (увеличивая счётчик). Если разрешений нет, поток блокируется до их появления.

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

Основные операции с семафором

  1. Инициализация: Устанавливается начальное количество разрешений.
  2. Ожидание / Захват (Wait): Поток запрашивает разрешение. Если счётчик > 0, он уменьшается, и поток продолжает работу. Если счётчик = 0, поток блокируется.
  3. Освобождение (Release): Поток возвращает разрешение, увеличивая счётчик и потенциально разблокируя ожидающие потоки.

Типы семафоров

  • Счётчиковый семафор: Может иметь любое неотрицательное значение счётчика, используется для ограничения доступа к пулу ресурсов (например, подключений к БД).
  • Двоичный семафор: Частный случай, где счётчик равен 0 или 1. Функционально близок к мьютексу, но с ключевым отличием: семафор может быть освобождён любым потоком, а не только тем, который его захватил.

Семафор в C# (System.Threading.Semaphore и SemaphoreSlim)

В C# есть два основных класса для работы с семафорами:

SemaphoreSlim (рекомендуется для внутрипроцессного использования)

Легковесная версия, оптимизированная для работы в рамках одного процесса.

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

class Program
{
    // Семафор, позволяющий одновременно работать не более 3 потокам
    static readonly SemaphoreSlim _semaphore = new SemaphoreSlim(initialCount: 3);

    static async Task Main()
    {
        Task[] tasks = new Task[10];
        for (int i = 0; i < tasks.Length; i++)
        {
            int taskId = i;
            tasks[i] = Task.Run(() => AccessDatabase(taskId));
        }

        await Task.WhenAll(tasks);
        Console.WriteLine("Все задачи завершены.");
    }

    static async Task AccessDatabase(int id)
    {
        Console.WriteLine($"Задача {id} ждёт доступа к БД...");

        await _semaphore.WaitAsync(); // Асинхронное ожидание разрешения
        try
        {
            Console.WriteLine($"Задача {id} начала работу с БД. [Разрешённых потоков: {3 - _semaphore.CurrentCount}]");
            await Task.Delay(1000); // Имитация работы
        }
        finally
        {
            _semaphore.Release(); // Критически важно освободить семафор в finally
            Console.WriteLine($"Задача {id} освободила семафор.");
        }
    }
}

Semaphore (для межпроцессной синхронизации)

Более тяжеловесный класс, который может синхронизировать потоки между разными процессами на одном компьютере (именованный семафор).

// Создание или открытие именованного системного семафора
using (var semaphore = new Semaphore(initialCount: 2, maximumCount: 2, name: "MyGlobalSemaphore"))
{
    Console.WriteLine("Ожидание доступа к межпроцессному ресурсу...");
    semaphore.WaitOne();
    try
    {
        // Работа с общим ресурсом между процессами
        Console.WriteLine("Ресурс захвачен. Работаем...");
        Thread.Sleep(2000);
    }
    finally
    {
        semaphore.Release();
        Console.WriteLine("Ресурс освобождён.");
    }
}

Ключевые аспекты использования

  • Идиома Wait/Release в try-finally: Всегда освобождайте семафор в блоке finally, чтобы гарантировать его освобождение даже при возникновении исключения.
  • Асинхронное ожидание: SemaphoreSlim предоставляет метод WaitAsync(), который следует использовать в асинхронном коде для избежания блокировки потоков.
  • Отличие от мьютекса: Семафор не имеет концепции "владельца" — любой поток может вызвать Release(). Мьютекс (Mutex или lock) должен освобождаться тем же потоком, который его захватил.
  • Типичные сценарии применения:
    *   Ограничение количества одновременных подключений к базе данных или внешнему API.
    *   Управление доступом к пулу ресурсов (например, сокетов или файловых дескрипторов).
    *   Реализация паттерна "Producer-Consumer" с ограниченной ёмкостью буфера.

Важные предостережения

  • Не допускайте утечек разрешений: Каждый вызов Wait()/WaitAsync() должен иметь парный вызов Release(). Неосвобождение семафора приводит к "зависанию" потоков или постепенному исчерпанию ресурсов.
  • Инициализация: Начальное значение счётчика (initialCount) не должно превышать максимальное (maximumCount).
  • Производительность: Для синхронизации в рамках одного процесса всегда предпочитайте SemaphoreSlim — он значительно быстрее Semaphore.

Таким образом, семафор является гибким и мощным инструментом для управления параллельным доступом, когда необходимо ограничить, но не полностью исключить одновременную работу потоков над общим ресурсом. Его правильное использование — важный навык для разработки надёжных многопоточных приложений на C#.