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

В какую конструкцию разворачивается lock?

2.2 Middle🔥 152 комментариев
#Асинхронность и многопоточность#Основы C# и .NET

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

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

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

Конструкция lock в C# является синтаксическим сахаром и разворачивается компилятором в использование класса System.Threading.Monitor, который предоставляет низкоуровневые примитивы синхронизации потоков. Давайте подробно рассмотрим эту трансформацию.

Как работает lock

Когда вы пишете код с lock:

private readonly object _lockObj = new object();

public void ThreadSafeMethod()
{
    lock (_lockObj)
    {
        // Критическая секция
        DoWork();
    }
}

Компилятор C# преобразует его примерно в следующий эквивалент:

public void ThreadSafeMethod()
{
    bool lockTaken = false;
    object temp = _lockObj;
    
    try
    {
        // Попытка захвата монитора
        System.Threading.Monitor.Enter(temp, ref lockTaken);
        
        // Критическая секция
        DoWork();
    }
    finally
    {
        // Гарантированное освобождение монитора
        if (lockTaken)
        {
            System.Threading.Monitor.Exit(temp);
        }
    }
}

Подробности развертывания

1. Захват монитора с безопасным паттерном

Компилятор использует перегрузку Monitor.Enter с параметром ref bool lockTaken. Это критически важно для предотвращения следующих проблем:

  • Прерывание потока: Если поток будет прерван (например, Thread.Abort) между вызовами Enter и установкой флага, без этого паттерна монитор мог бы остаться захваченным
  • Исключения в критической секции: Гарантия освобождения в блоке finally

2. Локальная копия объекта блокировки

Обратите внимание на создание локальной переменной temp:

object temp = _lockObj;

Это предотвращает потенциальную проблему, если поле _lockObj будет переназначено другим потоком после проверки на null, но до захвата монитора.

3. Освобождение в блоке finally

Блок finally гарантирует, что монитор будет освобожден даже при возникновении исключения в критической секции, что предотвращает взаимные блокировки (deadlocks).

Что делает Monitor внутри

Реализация монитора

Класс Monitor использует несколько ключевых механизмов:

  1. Синхроблок (SyncBlock): Каждый объект в .NET имеет заголовок, который содержит индекс в таблице SyncBlock. Когда вызывается Monitor.Enter, система:

    • Проверяет, свободен ли SyncBlock объекта
    • Если свободен - захватывает его для текущего потока
    • Если занят - поток переходит в состояние ожидания
  2. Очередь ожидания: Потоки, ожидающие один и тот же объект, помещаются в очередь готовности.

  3. Рекурсивный захват: Monitor поддерживает рекурсивные захваты - один поток может многократно вызывать Enter для одного объекта, но должен вызвать Exit соответствующее количество раз.

Важные нюансы использования

Правила для объекта блокировки:

// НЕПРАВИЛЬНО - строка интернируется и может быть общей
lock ("myLock") { }

// НЕПРАВИЛЬНО - типы могут использоваться в нескольких местах
lock (typeof(MyClass)) { }

// ПРАВИЛЬНО - выделенный private объект
private readonly object _syncRoot = new object();

// ПРАВИЛЬНО для коллекций (если не используется Concurrent-версия)
private List<int> _list = new List<int>();
lock (((ICollection)_list).SyncRoot) { }

Альтернативы lock

В современных версиях C# и .NET существуют альтернативы:

  • SemaphoreSlim: Для ограничения доступа к ресурсу определенным количеством потоков
  • ReaderWriterLockSlim: Для сценариев "много читателей, один писатель"
  • Mutex: Для межпроцессной синхронизации
  • Concurrent-коллекции: Не требуют явной синхронизации

Сравнение производительности

lockMonitor) имеют относительно низкие накладные расходы по сравнению с другими примитивами синхронизации:

  1. Быстрый путь: Если монитор свободен, захват происходит быстро (десятки наносекунд)
  2. Медленный путь: Если монитор занят, поток переходит в состояние ожидания, что требует переключения контекста

Резюме

Конструкция lock разворачивается в шаблонный код с использованием Monitor.Enter/Monitor.Exit, обернутый в блоки try-finally для гарантированного освобождения ресурса. Это обеспечивает:

  • Потокобезопасность за счет исключительного доступа к критической секции
  • Надежность через безопасный паттерн с lockTaken
  • Производительность через оптимизированную реализацию монитора в CLR

Понимание этого преобразования важно для написания корректных многопоточных приложений и отладки сложных проблем синхронизации.