Вчера я говорил о новой функции асинхронности в C #, в частности, о том, как выглядит сгенерированный код, и the GetAwaiter()
/ BeginAwait()
/ EndAwait()
вызовах.
Мы подробно рассмотрели конечный автомат, сгенерированный компилятором C #, и мы не могли понять два аспекта:
- Почему сгенерированный класс содержит
Dispose()
метод и$__disposing
переменную, которые никогда не используются (а класс не реализуетсяIDisposable
). - Почему внутренняя
state
переменная установлена в 0 перед любым вызовомEndAwait()
, когда 0 обычно означает «это начальная точка входа».
Я подозреваю, что на первый вопрос можно ответить, сделав что-то более интересное в асинхронном методе, хотя, если у кого-то будет какая-либо дополнительная информация, я буду рад ее услышать. Этот вопрос больше касается второго пункта, однако.
Вот очень простой пример кода:
using System.Threading.Tasks;
class Test
{
static async Task<int> Sum(Task<int> t1, Task<int> t2)
{
return await t1 + await t2;
}
}
... и вот код, который генерируется для MoveNext()
метода, который реализует конечный автомат. Это скопировано непосредственно из Reflector - я не исправил невыразимые имена переменных:
public void MoveNext()
{
try
{
this.$__doFinallyBodies = true;
switch (this.<>1__state)
{
case 1:
break;
case 2:
goto Label_00DA;
case -1:
return;
default:
this.<a1>t__$await2 = this.t1.GetAwaiter<int>();
this.<>1__state = 1;
this.$__doFinallyBodies = false;
if (this.<a1>t__$await2.BeginAwait(this.MoveNextDelegate))
{
return;
}
this.$__doFinallyBodies = true;
break;
}
this.<>1__state = 0;
this.<1>t__$await1 = this.<a1>t__$await2.EndAwait();
this.<a2>t__$await4 = this.t2.GetAwaiter<int>();
this.<>1__state = 2;
this.$__doFinallyBodies = false;
if (this.<a2>t__$await4.BeginAwait(this.MoveNextDelegate))
{
return;
}
this.$__doFinallyBodies = true;
Label_00DA:
this.<>1__state = 0;
this.<2>t__$await3 = this.<a2>t__$await4.EndAwait();
this.<>1__state = -1;
this.$builder.SetResult(this.<1>t__$await1 + this.<2>t__$await3);
}
catch (Exception exception)
{
this.<>1__state = -1;
this.$builder.SetException(exception);
}
}
Это долго, но важные строки для этого вопроса таковы:
// End of awaiting t1
this.<>1__state = 0;
this.<1>t__$await1 = this.<a1>t__$await2.EndAwait();
// End of awaiting t2
this.<>1__state = 0;
this.<2>t__$await3 = this.<a2>t__$await4.EndAwait();
В обоих случаях состояние снова изменяется, прежде чем оно, очевидно, будет наблюдаться ... так зачем вообще устанавливать его в 0? Если бы MoveNext()
в этот момент вызывали снова (напрямую или через Dispose
), он снова эффективно запускал бы асинхронный метод, что, насколько я могу сказать, было бы совершенно неуместным ... если и MoveNext()
не вызывается , изменение состояния не имеет значения.
Является ли это просто побочным эффектом повторного использования компилятором кода генерации блока итератора для асинхронного преобразования, где это может иметь более очевидное объяснение?
Важный отказ от ответственности
Очевидно, это всего лишь компилятор CTP. Я полностью ожидаю, что все изменится до окончательного выпуска - и, возможно, даже до следующего выпуска CTP. Этот вопрос никоим образом не пытается утверждать, что это недостаток компилятора C # или чего-то в этом роде. Я просто пытаюсь понять, есть ли тонкая причина для этого, которую я пропустил :)
источник
Ответы:
Хорошо, у меня наконец есть реальный ответ. Я как-то решил это сам, но только после того, как Лучиан Висчик из команды VB подтвердил, что для этого есть все основания. Большое ему спасибо - и, пожалуйста, посетите его блог , который качается.
Значение 0 здесь только специальное, потому что это недопустимое состояние, в котором вы можете находиться перед
await
обычным случаем. В частности, это не состояние, которое конечный автомат может в конечном итоге проверить в другом месте. Я считаю, что использование любого неположительного значения будет работать так же хорошо: -1 не используется для этого, поскольку это логически неверно, так как -1 обычно означает «закончено». Я мог бы поспорить, что в данный момент мы придаем дополнительный смысл состоянию 0, но в конечном итоге это не имеет значения. Смысл этого вопроса состоял в том, чтобы выяснить, почему государство устанавливается вообще.Значение имеет значение, если ожидание заканчивается исключением, которое перехватывается. Мы можем в конечном итоге вернуться к тому же самому заявлению await снова, но мы не должны находиться в состоянии, означающем «Я только что вернусь из этого await», так как иначе все виды кода будут пропущены. Проще всего показать это на примере. Обратите внимание, что сейчас я использую второй CTP, поэтому сгенерированный код немного отличается от кода, приведенного в вопросе.
Вот асинхронный метод:
Концептуально, это
SimpleAwaitable
может быть любое ожидание - может быть, задача, может быть, что-то еще. В целях моих тестов он всегда возвращает false дляIsCompleted
и выдает исключение вGetResult
.Вот сгенерированный код для
MoveNext
:Мне пришлось перейти
Label_ContinuationPoint
к тому, чтобы сделать его допустимым кодом - иначе это не входит в сферуgoto
утверждения - но это не влияет на ответ.Подумайте о том, что происходит, когда
GetResult
выкидывает свое исключение. Мы пройдемся по блоку catch, инкрементуi
, а затем снова зациклимся (при условии,i
что все еще меньше 3). Мы все еще находимся в том состоянии, в котором мы были доGetResult
вызова ... но когда мы попадаем внутрьtry
блока, мы должны вывести "In Try" иGetAwaiter
снова вызвать ... и мы сделаем это, только если состояние не равно 1. Безstate = 0
задание, он будет использовать существующую awaiter и пропуститьConsole.WriteLine
звонок.Это довольно трудоемкий код, который нужно проработать, но он показывает, что нужно думать команде. Я рад, что не несу ответственности за это :)
источник
если он будет равен 1 (первый случай), вы получите вызов
EndAwait
без вызоваBeginAwait
. Если его оставить равным 2 (второй случай), вы получите тот же результат только для другого ожидающего.Я предполагаю, что вызов BeginAwait возвращает false, если он уже был запущен (предположение с моей стороны), и сохраняет исходное значение для возврата в EndAwait. Если это так, он будет работать правильно, тогда как если вы установите его на -1, то
this.<1>t__$await1
для первого случая вы можете иметь неинициализированный код .Это, однако, предполагает, что BeginAwaiter фактически не будет запускать действие для каких-либо вызовов после первого и что в этих случаях он вернет false. Начало, конечно, было бы неприемлемо, поскольку оно могло бы иметь побочный эффект или просто дать другой результат. Также предполагается, что EndAwaiter всегда будет возвращать одно и то же значение независимо от того, сколько раз оно вызывается, и это можно вызвать, когда BeginAwait возвращает false (согласно приведенному выше предположению).
Это может показаться защитой от условий гонки. Если мы добавим операторы, в которых movenext вызывается другим потоком после состояния = 0, то в вопросах это будет выглядеть примерно так:
Если приведенные выше предположения верны, выполняется некоторая ненужная работа, такая как get sawiater и переназначение того же значения на <1> t __ $ await1. Если бы состояние сохранялось на 1, то последняя часть была бы:
далее, если он был установлен в 2, конечный автомат предположил бы, что он уже получил значение первого действия, которое было бы неверным, и (потенциально) неназначенная переменная использовалась бы для вычисления результата
источник
Может быть это как-то связано со сложенными / вложенными асинхронными вызовами? ..
то есть:
В такой ситуации вызывается ли делегат movenext несколько раз?
Просто пунт на самом деле?
источник
MoveNext()
будет вызван один раз на каждого из них.Объяснение актуальных состояний:
возможные состояния:
Возможно ли, что эта реализация просто хочет гарантировать, что, если произойдет еще один вызов MoveNext от того, где бы он ни находился (во время ожидания), он заново пересмотрит всю цепочку состояний с самого начала, чтобы переоценить результаты, которые могут быть тем временем уже устаревшими?
источник