Информация об изменениях

Сообщение Re: Почему не следует смешивать async-await и lock? от 10.02.2019 23:50

Изменено 10.02.2019 23:59 MozgC

Re: Почему не следует смешивать async-await и lock?
Здравствуйте, another_coder, Вы писали:

_>Я читал о том, что подобный код нормально работать не будет

_>
_>object lockObj = new object()
_>lock(lockObj)
_>{
_>  await AsyncMethod();
_>}
_>


Такой код даже не скомпилируется. Комплилятор запрещает использование await внутри lock().

Почему?

Рассмотрим, что происходит в строчке "await AsyncMethod()".
Допустим, у нас такой код:

async void SomeMethod()
{
  doSomething1();
  await AsyncMethod();
  doSomething2();
}

Task AsyncMethod()
{
  return Task.Run(() => { do something on another thread })
}


На месте кода "await AsyncMethod()" компилятором будет сделано следующее:

Вначале компилятор создаст конечный автомат. Это будет просто объект класса, реализующий интерфейс типа IAsyncStateMachine. При создании этого конечного автомата будет произведена попытка захвата текущего контекста синхронизации. Этот контекст синхронизации будет использоваться при продолжении выполнении кода, следующего после await. Поэтому, если await выполняется например в UI потоке, то и продолжение выполнения будет произведено в этом же UI потоке. Если контекста синхронизации на момент создания конечного автомата нет, то выполнение продолжится в том же потоке, в котором выполнялся и асихронный метод. Поэтому, кстати, можно нечаянно нежелательно нагрузить ThreadPool тяжелыми операциями (что не советуется).

После того как контекст синхронизации и локальные переменные захвачены (чтобы можно потом было восстановить исполнение кода со старыми значениями локальных переменных), компилятор выполнит первый шаг конечного автомата. На этом шаге в нашем примере будет запущен Task, который запустит выполнение кода в другом потоке (это может быть thread pool, по-умолчанию, или может быть создан отдельный поток, если Task указан как long-running).

После этого метод SomeMethod() как бы досрочно прерывает выполнение. Т.е. можно представить, что перед doSomething2() стоит return; Если, например, метод SomeMethod() — это какой-то UI-обработчик, то мы вернемся в windows message loop, где продолжим обрабатывать следующие события из очереди.
Выполнение SomeMethod() будет возвращено, когда Task внутри AsyncMethod закончит работу и будет выполнен следующий шаг конечного автомата. Как я написал выше, поток, в котором будет возвращено исполнение будет зависеть от того, был ли контекст синхронизации на момент создания конечного автомата.

После того, как мы помедитировали над написанным выше, вернёмся к вопросу почему нельзя использовать await внутри lock();

Как мы тепрерь знаем, мы можем продолжить выполнение, не на том потоке, в котором мы захватили lock. Если обмануть компилятор, и вместо lock написать явно вызовы Monitor.Enter() и Monitor.Exit(), то если выполнение продолжится в другом потоке, то Monitor.Exit() выбросит исключение. Даже если бы Monitor.Exit() не выбрасывал исключение, то это уже неправильно, что в коде, который должен одновременно выполняться только одним потоком (раз мы хотим использовать lock), вдруг оказался другой поток.

Поэтому использование await внутри lock запретили.
Re: Почему не следует смешивать async-await и lock?
Здравствуйте, another_coder, Вы писали:

_>Я читал о том, что подобный код нормально работать не будет

object lockObj = new object()
lock(lockObj)
{
  await AsyncMethod();
}


Такой код даже не скомпилируется. Комплилятор запрещает использование await внутри lock().

Почему?

Рассмотрим, что происходит в строчке "await AsyncMethod()".
Допустим, у нас такой код:

async void SomeMethod()
{
  doSomething1();
  await AsyncMethod();
  doSomething2();
}

Task AsyncMethod()
{
  return Task.Run(() => { do something on another thread })
}


На месте кода "await AsyncMethod()" компилятором будет сделано следующее:

Вначале компилятор создаст конечный автомат. Это будет просто объект класса, реализующий интерфейс типа IAsyncStateMachine. При создании этого конечного автомата будет произведена попытка захвата текущего контекста синхронизации. Этот контекст синхронизации будет использоваться при продолжении выполнения кода, следующего после await. Поэтому, если await выполняется например в UI потоке, то и продолжение выполнения будет произведено в этом же UI потоке. Если контекста синхронизации на момент создания конечного автомата нет, то выполнение продолжится в том же потоке, в котором выполнялся и асихронный метод (т.е. в нашем примере это то, что вызывается внутри Task.Run()). Поэтому, кстати, можно нечаянно нежелательно нагрузить ThreadPool тяжелыми операциями (что не советуется).

После того, как контекст синхронизации и локальные переменные захвачены (чтобы можно потом было восстановить исполнение кода со старыми значениями локальных переменных), компилятор выполнит первый шаг конечного автомата. На этом шаге в нашем примере будет запущен Task, который запустит выполнение кода в другом потоке (это может быть thread pool, по-умолчанию, или может быть создан отдельный поток, если Task указан как long-running).

После этого метод SomeMethod() как бы досрочно прерывает выполнение. Т.е. можно представить, что перед doSomething2() стоит return; На самом деле, компилятор перенесет код, следующий после await, в конечный автомат.
Куда происходит возврат исполнения? Ну, например, если метод SomeMethod() — это какой-то UI-обработчик, то мы вернемся в windows message loop, где продолжим обрабатывать следующие события из очереди.
Выполнение SomeMethod() будет возвращено, когда Task внутри AsyncMethod закончит работу и будет выполнен следующий шаг конечного автомата. Как я написал выше, поток, в котором будет возвращено исполнение будет зависеть от того, был ли контекст синхронизации на момент создания конечного автомата.

После того, как мы помедитировали над написанным выше, вернёмся к вопросу почему нельзя использовать await внутри lock();

Как мы тепрерь знаем, выполнение может продолжиться не в том же потоке, в котором мы захватили lock. Если обмануть компилятор, и вместо lock написать явно вызовы Monitor.Enter() и Monitor.Exit(), то если выполнение продолжится в другом потоке, то Monitor.Exit() выбросит исключение. Даже если бы Monitor.Exit() не выбрасывал исключение, то это уже неправильно, что в коде, который должен целиком выполняться только одним потоком (раз мы хотим использовать lock), вдруг оказался другой поток.

Поэтому использование await внутри lock запретили.