Блокировка Async/Await

180
07 января 2022, 20:50

Читаю Рихтера и наткнулся на интересное ограничение асинхронной операции

Не допускается установление блокировки, поддерживающей владение потоком или рекурсию, до операции await, и ее снятие после оператора await. Это ограничение объясняется тем, что один поток может выполнить код до await, а другой поток может выполнить код после await. При использовании await с командой С# lock компилятор выдает сообщение об ошибке. Если вместо этого явно вызвать методы Enter и Exit класса Monitor, то код откомнилиру-

почему не допускается блокировка await это и есть deadlock???

Answer 1

Для начала простой пример:

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. Это не связано с дедлоками.

Answer 2

Дедлок- это когда поток 1 владеет ресурсом А и ждет, когда освободится ресурс Б, которым владеет поток 2, а тот в свою очередь ждет ресурс А, которым владеет поток 1. Круг замкнулся.

Фишка await в том, что до встречи await код может быть выполнен одним потоком, а после await продолжить выполнение в другом.

Из этого следует то, что другой поток попросту не сможет войти в блок синхронизации, так как им владеет другой поток.

Если рассмотреть то, во что компилируется этот синтаксический сахар в виде async/await, то такое ограничение станет еще более понятным.

Попросту, это все компилируется в конечный автомат из switch/case, где в некоторую переменную заносится флаг того, что такой-то код выполнен. Натыкаясь на await, происходит выход из метода и возвращение потока в пул. Когда задача выполнена, то этот метод снова дергается(возможно уже в другом потоке из пула) и на основании флага пропускаются этапы, которые ранее были выполнены и продолжается синхронная работа метода.

А теперь подумайте, как оно будет работать с lock'ом.

READ ALSO
Как соединить enum и тип double

Как соединить enum и тип double

Есть у меня такая конструкция

201
Goto на одну строку C#

Goto на одну строку C#

Вот допустим есть код:

93