Читаю Рихтера и наткнулся на интересное ограничение асинхронной операции
Не допускается установление блокировки, поддерживающей владение потоком или рекурсию, до операции await
, и ее снятие после оператора await
. Это ограничение объясняется тем, что один поток может выполнить код до await
,
а другой поток может выполнить код после await
. При использовании await
с командой С# lock
компилятор выдает сообщение об ошибке. Если вместо
этого явно вызвать методы Enter
и Exit
класса Monitor
, то код откомнилиру-
почему не допускается блокировка await это и есть deadlock???
Для начала простой пример:
static async Task Main(string[] args)
{
Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
await Task.Yield();
Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
}
Выполнив его, вы увидите 2 разных значения. Это значит, что код после оператора await
может выполнятся в другом потоке, нежели код до оператора.
Теперь попробуем сделать то, про что пишет Рихтер:
static async Task Main(string[] args)
{
var syncObject = new object();
Monitor.Enter(syncObject);
await Task.Yield();
Monitor.Exit(syncObject);
}
Получим SynchronizationLockException
. Что бы понять почему так происходит, нужно знать как работает Monitor.Enter
(ну и оператор lock
соответственно).
Когда выполняется метод Monitor.Enter
, CLR запоминает текущий поток в заголовке объекта (managed object header). Мы можем несколько раз вызывать Monitor.Enter
на одном и том же объекте в одном и том же потоке, и мы не будем получать блокировок. Код ниже отработает моментально:
static async Task Main(string[] args)
{
var syncObject = new object();
Monitor.Enter(syncObject);
Monitor.Enter(syncObject);
Monitor.Enter(syncObject);
}
Когда вызывается метод Monitor.Exit
, CLR проверяет, совпадает ли поток, который заблокировал объект, с потоком, который пытается разблокировать объект. Эта проверка как раз и гарантирует то, что только один поток может выполнять код в блоке lock (obj) { ... }
.
Что бы исключить внезапный SynchronizationLockException
и ввели ошибку компиляции связанную с операторами await
и lock
. Это не связано с дедлоками.
Дедлок- это когда поток 1 владеет ресурсом А и ждет, когда освободится ресурс Б, которым владеет поток 2, а тот в свою очередь ждет ресурс А, которым владеет поток 1. Круг замкнулся.
Фишка await
в том, что до встречи await
код может быть выполнен одним потоком, а после await
продолжить выполнение в другом.
Из этого следует то, что другой поток попросту не сможет войти в блок синхронизации, так как им владеет другой поток.
Если рассмотреть то, во что компилируется этот синтаксический сахар в виде async/await, то такое ограничение станет еще более понятным.
Попросту, это все компилируется в конечный автомат из switch/case
, где в некоторую переменную заносится флаг того, что такой-то код выполнен. Натыкаясь на await
, происходит выход из метода и возвращение потока в пул. Когда задача выполнена, то этот метод снова дергается(возможно уже в другом потоке из пула) и на основании флага пропускаются этапы, которые ранее были выполнены и продолжается синхронная работа метода.
А теперь подумайте, как оно будет работать с lock'ом
.
Айфон мало держит заряд, разбираемся с проблемой вместе с AppLab
Перевод документов на английский язык: Важность и ключевые аспекты