Какие режимы блокировки потоков существуют
Что проверяют
Понимаете ли вы, чем примитивы синхронизации в .NET отличаются по цене и области действия, и умеете ли выбрать нужный. Следом почти всегда спрашивают, почему нельзя сделать await внутри lock и что использовать вместо.
Как отвечать
Удобно разложить по режимам, в которых работает примитив:
- пользовательский режим:
Interlocked,volatile,SpinLock,SpinWait. Без перехода в ядро, очень дёшево, но ожидающий поток крутится и тратит CPU. Годится для коротких секций и счётчиков; - режим ядра:
Mutex,Semaphore,EventWaitHandle(AutoResetEvent,ManualResetEvent). Поток засыпает, каждый захват стоит системного вызова. Именованные варианты работают между процессами,Mutexпривязан к владельцу, и освободить его может только захвативший поток; - гибридные:
Monitor(он жеlock),SemaphoreSlim,ManualResetEventSlim,ReaderWriterLockSlim. Сначала короткое вращение, затем ожидание через ядро. Это выбор по умолчанию внутри процесса.
Дальше по задачам. Взаимное исключение: lock, в .NET 9 появился отдельный тип System.Threading.Lock, с которым lock компилируется в более быстрый EnterScope(). Ограничение числа параллельных операций и любой асинхронный код: SemaphoreSlim с WaitAsync. Много читателей и редкий писатель: ReaderWriterLockSlim, по умолчанию без рекурсии. Межпроцессная синхронизация: именованный Mutex. Простой счётчик: Interlocked.Increment, никакой блокировки не нужно.
Где спотыкаются
Пытаются сделать await внутри lock. Компилятор это запрещает, а обход через Monitor.Enter ломается: продолжение после await может выполниться на другом потоке, а монитор привязан к потоку-владельцу. Нужен SemaphoreSlim(1, 1) и Release() в finally.
Блокируются на this, строке или typeof(...). Такой объект виден внешнему коду, и чужой lock на нём даёт неожиданный дедлок. Нужен приватный объект только для синхронизации.
Хранят SpinLock в readonly-поле или передают по значению. Это структура: вызов идёт на копии, и синхронизации нет.