Можно ли использовать lock с примитивами синхронизации (например, ManualResetEvent, Semaphore)?

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

Ответ

Нельзя. Это классическая ошибка, ведущая к взаимоблокировкам (deadlock) и снижению производительности.

Почему это опасно: Примитивы синхронизации высшего уровня (ManualResetEvent, SemaphoreSlim, Mutex, AutoResetEvent) уже являются потокобезопасными и internally используют свои собственные механизмы синхронизации ядра ОС или пользовательского режима. Обёртывание их в lock создаёт избыточную и конфликтующую блокировку.

Пример Deadlock:

private object _lockObj = new object();
private ManualResetEvent _mre = new ManualResetEvent(false);

// Поток A
lock (_lockObj) // Захватывает _lockObj
{
    _mre.WaitOne(); // Блокируется, ожидая сигнала
}

// Поток B (который должен подать сигнал)
lock (_lockObj) // Вечно ждёт, пока Поток A освободит _lockObj -> DEADLOCK
{
    _mre.Set();
}

Правильный подход:

  1. Используйте примитив напрямую. Его API спроектирован для безопасного вызова из нескольких потоков.

    private SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1);
    
    public async Task AccessResourceAsync()
    {
        await _semaphore.WaitAsync(); // Достаточно одной операции
        try {
            // Критическая секция
        } finally {
            _semaphore.Release();
        }
    }
  2. Выбирайте примитив под задачу:
    • SemaphoreSlim — для ограничения одновременного доступа к ресурсу.
    • ManualResetEventSlim — для уведомления потоков о событии.
    • ReaderWriterLockSlim — для оптимизации сценариев "много чтений / редко запись".
    • lock (Monitor) — для быстрой низкоуровневой синхронизации простых операций над объектом.