В чём разница между Semaphore и SemaphoreSlim в .NET?

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

Ответ

Semaphore и SemaphoreSlim — это примитивы синхронизации, ограничивающие число потоков, которые могут одновременно получить доступ к ресурсу или пулу. Их ключевое отличие — в области применения и производительности.

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

  • Назначение: Легковесная версия, оптимизированная для сценариев в рамках одного процесса.
  • Производительность: Высокая, так как реализована в пользовательском режиме (user-mode) и обращается к ядру ОС только при блокировке.
  • Ключевая особенность: Поддерживает асинхронное ожидание через метод WaitAsync(), что критически важно для современных async/await паттернов.
  • Межпроцессность: Не поддерживает именованные семафоры, работает только внутри процесса.
// Пример: ограничение параллельных HTTP-запросов
private static readonly SemaphoreSlim _throttler = new SemaphoreSlim(5); // Макс 5 параллельных задач

public async Task<string> MakeThrottledRequestAsync(string url)
{
    await _throttler.WaitAsync(); // Асинхронное ожидание разрешения
    try
    {
        using var client = new HttpClient();
        return await client.GetStringAsync(url);
    }
    finally
    {
        _throttler.Release(); // Освобождение слота
    }
}

Semaphore (для сложных сценариев)

  • Назначение: Класс-обёртка над объектом ядра ОС Windows. Используется для межпроцессной синхронизации или когда нужны функции, специфичные для ядра.
  • Производительность: Ниже, чем у SemaphoreSlim, так как каждый вызов WaitOne()/Release() требует переключения в режим ядра.
  • Ключевая особенность: Поддерживает именованные семафоры, которые могут синхронизировать потоки разных процессов.
  • Асинхронность: Не имеет нативных асинхронных методов.

// Пример: межпроцессная синхронизация доступа к общему файлу
// Имя семафора должно быть уникальным в системе
using var semaphore = new Semaphore(1, 1, "Global\MyApp_FileAccessSemaphore");

semaphore.WaitOne(); // Синхронная блокировка (может быть долгой)
try
{
    // Критическая секция, доступная только одному процессу за раз
    File.AppendAllText("shared.log", $"{DateTime.UtcNow}: Запись из процесса {Process.GetCurrentProcess().Id}n");
}
finally

### Практическое правило
*   **Всегда начинайте с `SemaphoreSlim`**, если синхронизация нужна только внутри вашего приложения.
*   Используйте `Semaphore` только в двух случаях:
    1.  Требуется синхронизация между несколькими независимыми процессами.
    2.  Необходимо время ожидания, превышающее возможности `SemaphoreSlim` (что маловероятно).