Ответ
Нет, это запрещено и приведет к ошибке компиляции или серьезным runtime-проблемам, таким как взаимные блокировки (deadlock).
Почему нельзя:
- Механизм
lockв C# — это синтаксический сахар дляMonitor.EnterиMonitor.Exit. Эти операции привязаны к текущему потоку выполнения. - Ключевое слово
awaitприостанавливает выполнение текущего метода и возвращает управление потоку. Если это произойдет внутриlock, поток освободится, но монитор объекта останется захваченным этим потоком. - Когда асинхронная операция завершится, возобновление метода (
await) может произойти на другом потоке из пула потоков. Этот новый поток попытается вызватьMonitor.Exitдля объекта, который он никогда не захватывал, что приведет кSynchronizationLockException.
Пример проблемы:
private readonly object _syncLock = new object();
public async Task IncorrectMethodAsync()
{
lock (_syncLock) // Захватываем монитор в текущем потоке
{
// ОПАСНО! При await поток может быть освобожден.
await SomeAsyncOperation(); // <- Точка, где всё ломается
// Возобновление здесь может произойти на ДРУГОМ потоке.
} // Новый поток попытается освободить чужой lock -> Исключение!
}
Правильные альтернативы для асинхронной синхронизации:
1. Использование SemaphoreSlim с поддержкой асинхронности:
private readonly SemaphoreSlim _asyncLock = new SemaphoreSlim(1, 1);
public async Task CorrectMethodAsync()
{
await _asyncLock.WaitAsync(); // Асинхронное ожидание
try
{
// Критическая секция, безопасная для асинхронных операций
await SomeAsyncOperation();
await AnotherAsyncOperation();
}
finally
{
_asyncLock.Release(); // Гарантированное освобождение
}
}
2. Использование AsyncLock из сторонних библиотек (например, Nito.AsyncEx):
private readonly AsyncLock _mutex = new AsyncLock();
public async Task MethodWithAsyncLock()
{
using (await _mutex.LockAsync()) // Удобная обертка
{
await SomeAsyncOperation();
}
}
3. Перепроектирование кода: Часто необходимость в lock внутри асинхронного метода указывает на проблему с дизайном. Рассмотрите:
- Очереди (
Channel,BufferBlock): для обработки задач одним потребителем. - Конкурентные коллекции (
ConcurrentDictionary,ConcurrentQueue): если нужно только потокобезопасное хранилище. - Неизменяемые (immutable) структуры данных: чтобы полностью избежать состояния, требующего блокировки.
Ответ 18+ 🔞
А, слушай, вот тут у народа часто возникает гениальная идея — запихнуть await прямо внутрь lock. Ну, чтобы, типа, и синхронизация, и асинхронность, всё в одном флаконе, красота!
Так вот, запомни раз и навсегда: это пиздец, а не идея. Компилятор, конечно, поругается, но если каким-то чудом проскочишь — получишь runtime такое, что мало не покажется. Чаще всего — взаимную блокировку, deadlock, одним словом.
А почему, спрашивается, нельзя? Да всё просто, как три копейки:
lock— это обёртка. Под капотом тамMonitor.EnterиMonitor.Exit. И вся эта магия привязана к конкретному потоку, который её вызвал.await— это предатель. Как только ты его вызываешь, текущий метод приостанавливается, а поток-исполнитель благополучно сваливает делать другие дела. Но монитор-то объекта остаётся захваченным этим сбежавшим потоком! Он его как будто с собой унёс, сволочь.- А потом приходит новый поток. Операция завершилась, выполнение возобновляется. Но — сюрприз! — это уже другой поток из пула. И этот новенький, ни в чём не повинный поток, добравшись до закрывающей скобки
lock, попытается вызватьMonitor.Exitдля объекта, который он в жизни не захватывал. И получит по ебалуSynchronizationLockException. Красота.
Вот, смотри, как выглядит эта лажа в коде:
private readonly object _syncLock = new object();
public async Task IdiotMethodAsync()
{
lock (_syncLock) // Текущий поток захватил замок. Всё пучком.
{
// А вот тут начинается пиздец.
await SomeAsyncOperation(); // Поток говорит "Чао!" и уходит. Но замок-то он с собой не отпустил!
// ... а возобновимся мы тут, скорее всего, на каком-нибудь левом потоке №8.
} // Поток №8: "Так, надо освободить lock... Стоп. А я его вообще брал? Ой, бля."
// SynchronizationLockException, привет!
}
Так что же делать, если очень надо? Есть рабочие варианты:
1. SemaphoreSlim — наш бро.
У него есть специальный асинхронный метод WaitAsync(). Он для этого и создан.
private readonly SemaphoreSlim _asyncLock = new SemaphoreSlim(1, 1); // Один слот — как у обычного lock
public async Task NormalMethodAsync()
{
await _asyncLock.WaitAsync(); // Стоим в асинхронной очереди, не блокируя потоки.
try
{
// Теперь мы в безопасной критической секции. Можем хоть десять await'ов делать.
await SomeAsyncOperation();
await AnotherAsyncOperation();
}
finally
{
_asyncLock.Release(); // Освобождаем в любом случае. Это святое.
}
}
2. Готовые библиотеки, типа Nito.AsyncEx.
Там есть готовый AsyncLock, который делает ровно то, что ты хотел, только правильно.
private readonly AsyncLock _mutex = new AsyncLock();
public async Task MethodWithAsyncLock()
{
using (await _mutex.LockAsync()) // Красиво и понятно.
{
await SomeAsyncOperation();
} // Dispose сам всё отпустит — удобно.
}
3. Вообще переосмыслить архитектуру. Часто такой вопрос — это звоночек, что дизайн пошёл по кривой дорожке. Может, вместо общего изменяемого состояния с блокировками использовать:
- Очереди (
Channel<T>,BufferBlock<T>) — пусть задачи выстраиваются в линеечку и обрабатываются по одной. - Конкурентные коллекции (
ConcurrentDictionary,ConcurrentQueue) — если нужно просто потокобезопасное хранилище. - Неизменяемые (immutable) данные — идеальный вариант. Нет изменяемого состояния — не нужны и блокировки, хоть синхронные, хоть асинхронные.