Как устроен lock и как поток понимает необходимость ожидания
Что проверяют
Понимаете ли вы, что lock это синтаксис над Monitor, и что происходит на уровне объекта и потока, когда блокировка занята. Следом обычно спрашивают, почему внутри lock нельзя await и что использовать вместо него в асинхронном коде.
Как отвечать
Компилятор разворачивает lock (obj) { ... } в Monitor.Enter(obj, ref lockTaken) и try/finally с Monitor.Exit(obj), поэтому блокировка снимается даже при исключении.
Как поток узнаёт, что надо ждать:
- заголовок объекта: у каждого объекта в куче есть заголовок. При захвате поток атомарной операцией сравнения и обмена (CAS) записывает туда свой идентификатор, это так называемая тонкая блокировка;
- занято: если CAS не прошёл и в заголовке чужой идентификатор, поток понимает, что монитор занят. Сначала он недолго крутится в цикле ожидания, рассчитывая, что владелец скоро выйдет: это дешевле переключения контекста;
- инфляция: если ждать приходится долго или конкурентов несколько, блокировка раздувается до sync block с объектом ожидания ядра, и поток засыпает, не тратя процессор;
- освобождение:
Monitor.Exitсбрасывает владельца и будит одного из ожидающих. Порядок не гарантирован, честной очереди нет; - реентерабельность: тот же поток может войти в
lockповторно, монитор ведёт счётчик захватов.
Для ожидания условия внутри блокировки есть Monitor.Wait и Monitor.Pulse. В .NET 9 появился тип System.Threading.Lock: если передать в lock его экземпляр, компилятор вызовет EnterScope() вместо Monitor, это быстрее и явнее. В асинхронном коде вместо lock берут SemaphoreSlim(1, 1) с await WaitAsync().
Где спотыкаются
Пишут await внутри lock. Компилятор это запрещает, и не случайно: продолжение может выполниться на другом потоке, а монитор привязан к потоку-владельцу.
Блокируют this, typeof(T) или строку. Такой объект доступен внешнему коду, строки к тому же интернируются, и чужой код может взять ту же блокировку. Правильно блокировать приватное readonly-поле.
Предотвращают deadlock только советом «держать lock коротким». Нужен единый порядок захвата нескольких блокировок, а для диагностики Monitor.TryEnter с таймаутом.