Опыт использования примитивов синхронизации потоков
Что проверяют
Знаете ли вы, что стоит за lock, и умеете ли выбирать примитив по задаче, а не по первому знакомому имени. Отдельно проверяют асинхронный код: в мире async/await половина привычных примитивов неприменима, и это хорошо видно по ответу.
Как отвечать
Начните с того, что lock это синтаксис над Monitor, то есть внутрипроцессная блокировка на заголовке объекта. Всё остальное подбирается под задачу: SemaphoreSlim ограничивает число одновременных участников и умеет WaitAsync, поэтому именно он годится в асинхронном коде; ReaderWriterLockSlim окупается, когда читателей заметно больше писателей; Interlocked закрывает счётчики и обмен значением без блокировки вовсе; ManualResetEventSlim и AutoResetEvent про сигналы, а не про взаимное исключение.
Отдельно скажите про Mutex: это объект ядра, он дороже lock на порядок, но умеет то, чего не умеет lock, то есть синхронизацию между процессами по имени. Внутри одного процесса он не нужен. В свежих версиях платформы появился отдельный тип Lock со своим EnterScope, о нём стоит знать.
Полезно упомянуть, что часть задач решается без примитивов: ConcurrentDictionary, Channel для очереди производитель-потребитель, CancellationToken для отмены.
Где спотыкаются
Пытаются дождаться асинхронной операции внутри lock. Компилятор не пропускает await в теле блокировки, и вместо смены примитива пишут .Result или .Wait(), получая классический дедлок на контексте синхронизации. Правильный ответ здесь SemaphoreSlim с WaitAsync и освобождением в finally.
Блокируются на публично доступном объекте: lock (this), lock (typeof(Foo)), блокировка на строке. Любой внешний код может захватить тот же монитор, и причину взаимной блокировки потом не найти. Нужен приватный readonly object, недоступный снаружи.
Считают, что volatile делает операцию атомарной. Он говорит только про порядок и видимость чтений и записей, а count++ остаётся тремя действиями, и без Interlocked.Increment или блокировки инкремент теряется под нагрузкой.