Почему запрещено выполнять асинхронные операции внутри lock
Что проверяют
Знаете ли вы, что это не совет, а ошибка компиляции, и понимаете ли причину. Ответ уровня «так делать нехорошо» сразу показывает, что человек не разбирался, как lock устроен внутри.
Как отвечать
Скажите прямо: await в теле lock не компилируется. Причина в семантике монитора. lock разворачивается в Monitor.Enter и Monitor.Exit в блоке finally, а монитор привязан к потоку: освободить его обязан тот же поток, который захватил. После await продолжение вполне может выполниться на другом потоке пула, и выход из блокировки станет попыткой освободить чужое, то есть SynchronizationLockException. Плюс монитор рекурсивен по потоку, а асинхронный код такой гарантии не даёт вовсе.
Дальше назовите правильный инструмент: SemaphoreSlim с WaitAsync, он к потоку не привязан.
await _gate.WaitAsync(ct);
try { await DoWorkAsync(ct); }
finally { _gate.Release(); }Освобождение обязательно в finally, иначе первое же исключение оставит семафор захваченным навсегда. И общий принцип: под блокировкой держат только работу с разделяемым состоянием в памяти, а поход в сеть или в базу выносят наружу.
Где спотыкаются
Обходят запрет, запуская задачу без await внутри lock, а ожидая её уже снаружи. Это компилируется, но блокировка снимается раньше, чем задача закончила менять состояние, и защищать оказывается нечего.
Второе: вместо семафора берут Mutex. У него та же привязка к потоку, что у монитора, плюс он межпроцессный и потому заметно дороже. Для синхронизации внутри процесса это худший выбор из возможных.
Третье: считают SemaphoreSlim заменой lock один в один и ловят взаимоблокировку на рекурсии. Монитор пускает тот же поток повторно, семафор не пускает никого: вложенный вызов того же метода зависнет на собственном захвате навсегда.