C#5 async CTP: ¿por qué el "estado" interno se establece en 0 en el código generado antes de la llamada EndAwait?
asynchronous c#-5.0 (4)
Ayer estuve dando una charla sobre la nueva característica "asincrónica" de C #, en particular profundizando en cómo se veía el código generado, y the GetAwaiter()
/ BeginAwait()
/ EndAwait()
.
Analizamos en detalle la máquina de estados generada por el compilador de C #, y había dos aspectos que no podíamos entender:
- Por qué la clase generada contiene un método
Dispose()
y una variable$__disposing
, que nunca parece ser utilizada (y la clase no implementaIDisposable
). - Por qué la variable de
state
interno se establece en 0 antes de cualquier llamada aEndAwait()
, cuando 0 normalmente parece indicar "este es el punto de entrada inicial".
Sospecho que el primer punto podría ser respondido haciendo algo más interesante dentro del método asincrónico, aunque si alguien tiene más información, me alegraría escucharlo. Sin embargo, esta pregunta es más sobre el segundo punto.
Aquí hay una pieza muy simple de código de muestra:
using System.Threading.Tasks;
class Test
{
static async Task<int> Sum(Task<int> t1, Task<int> t2)
{
return await t1 + await t2;
}
}
... y aquí está el código que se genera para el método MoveNext()
que implementa la máquina de estados. Esto se copia directamente de Reflector: no he arreglado los nombres de las variables indescriptibles:
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);
}
}
Es largo, pero las líneas importantes para esta pregunta son estas:
// 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();
En ambos casos, el estado se cambia nuevamente luego, obviamente, antes de que se observe ... entonces, ¿por qué establecerlo en 0? Si se llamara MoveNext()
nuevamente en este punto (ya sea directamente o mediante Dispose
), comenzaría de nuevo el método async, lo que sería completamente inapropiado por lo que yo sé ... if y MoveNext()
no se llama, el cambio de estado es irrelevante.
¿Es esto simplemente un efecto secundario del compilador que reutiliza el código de generación de bloques del iterador para asincrónico, donde puede tener una explicación más obvia?
Importante descargo de responsabilidad
Obviamente, esto es solo un compilador CTP. Espero totalmente que las cosas cambien antes de la versión final, y posiblemente incluso antes de la próxima versión de CTP. Esta pregunta de ninguna manera intenta afirmar que esto es un defecto en el compilador de C # o algo así. Solo estoy tratando de averiguar si hay una razón sutil para esto que me he perdido :)
¿Podría ser algo relacionado con las llamadas asincrónicas apiladas / anidadas?
es decir:
async Task m1()
{
await m2;
}
async Task m2()
{
await m3();
}
async Task m3()
{
Thread.Sleep(10000);
}
¿Se llama al delegado movenext varias veces en esta situación?
Solo un despeje realmente?
De acuerdo, finalmente tengo una respuesta real. Lo resolví por mi cuenta, pero solo después de que Lucian Wischik de la parte del equipo de VB confirmara que realmente hay una buena razón para ello. Muchas gracias a él, y por favor visite su blog , que es genial.
El valor 0 aquí solo es especial porque no es un estado válido en el que puede estar justo antes de la await
en un caso normal. En particular, no es un estado que la máquina de estado puede terminar probando en otro lugar. Creo que usar cualquier valor no positivo funcionaría igual de bien: -1 no se usa para esto ya que es lógicamente incorrecto, ya que -1 normalmente significa "terminado". Podría argumentar que estamos dando un significado extra para decir 0 en este momento, pero en última instancia, realmente no importa. El objetivo de esta pregunta fue descubrir por qué el estado se está estableciendo en absoluto.
El valor es relevante si la espera termina en una excepción que se captura. Podemos terminar volviendo a la misma declaración de espera otra vez, pero no debemos estar en el estado que significa "Estoy a punto de volver de esa espera", ya que de lo contrario se omitirían todas las clases de código. Es más simple mostrar esto con un ejemplo. Tenga en cuenta que ahora estoy usando el segundo CTP, por lo que el código generado es ligeramente diferente al de la pregunta.
Este es el método asíncrono:
static async Task<int> FooAsync()
{
var t = new SimpleAwaitable();
for (int i = 0; i < 3; i++)
{
try
{
Console.WriteLine("In Try");
return await t;
}
catch (Exception)
{
Console.WriteLine("Trying again...");
}
}
return 0;
}
Conceptualmente, SimpleAwaitable
puede ser cualquier SimpleAwaitable
se pueda esperar, tal vez una tarea, tal vez otra cosa. A los efectos de mis pruebas, siempre devuelve falso para IsCompleted
y arroja una excepción en GetResult
.
Aquí está el código generado para MoveNext
:
public void MoveNext()
{
int returnValue;
try
{
int num3 = state;
if (num3 == 1)
{
goto Label_ContinuationPoint;
}
if (state == -1)
{
return;
}
t = new SimpleAwaitable();
i = 0;
Label_ContinuationPoint:
while (i < 3)
{
// Label_ContinuationPoint: should be here
try
{
num3 = state;
if (num3 != 1)
{
Console.WriteLine("In Try");
awaiter = t.GetAwaiter();
if (!awaiter.IsCompleted)
{
state = 1;
awaiter.OnCompleted(MoveNextDelegate);
return;
}
}
else
{
state = 0;
}
int result = awaiter.GetResult();
awaiter = null;
returnValue = result;
goto Label_ReturnStatement;
}
catch (Exception)
{
Console.WriteLine("Trying again...");
}
i++;
}
returnValue = 0;
}
catch (Exception exception)
{
state = -1;
Builder.SetException(exception);
return;
}
Label_ReturnStatement:
state = -1;
Builder.SetResult(returnValue);
}
Tuve que mover Label_ContinuationPoint
para que sea un código válido; de lo contrario, no está dentro del alcance de la declaración goto
, pero eso no afecta la respuesta.
Piensa en lo que sucede cuando GetResult
lanza su excepción. Pasaremos por el bloque catch, incrementaremos i
, y luego daremos un rodeo de nuevo (suponiendo que todavía sea menor que 3). Todavía estamos en el estado en que nos GetResult
antes de la llamada a GetResult
... pero cuando GetAwaiter
al bloque try
, debemos imprimir "In Try" y llamar a GetAwaiter
nuevamente ... y solo lo haremos si el estado no es GetAwaiter
. 1. Sin la asignación state = 0
, usará el awaiter existente y omitirá la llamada Console.WriteLine
.
Es un código bastante tortuoso para trabajar, pero eso solo muestra el tipo de cosas en las que el equipo tiene que pensar. Me alegro de no ser responsable de implementar esto :)
Explicación de estados reales:
estados posibles:
- 0 Inicializado (creo) o esperando el final de la operación
- > 0 acaba de llamar a MoveNext, eligiendo el siguiente estado
- -1 terminó
¿Es posible que esta implementación solo quiera asegurar que si otra llamada a MoveNext desde donde sea que ocurra (mientras espera) vuelva a evaluar toda la cadena de estado desde el principio, para reevaluar los resultados que podrían estar ya desactualizados?
si se mantuvo en 1 (primer caso), recibirás una llamada a EndAwait
sin una llamada a BeginAwait
. Si se mantiene en 2 (segundo caso) obtendrá el mismo resultado solo en el otro awaiter.
Supongo que llamar a BeginAwait devuelve falso si ya se ha iniciado (una suposición de mi parte) y conserva el valor original para regresar en EndAwait. Si ese es el caso, funcionaría correctamente, mientras que si lo configuras en -1, es posible que hayas uninicializado this.<1>t__$await1
para el primer caso.
Sin embargo, esto supone que BeginAwaiter en realidad no iniciará la acción en ninguna llamada después de la primera y que devolverá falsa en esos casos. Por supuesto, comenzar sería inaceptable, ya que podría tener un efecto secundario o simplemente dar un resultado diferente. También asume que EndAwaiter siempre devolverá el mismo valor sin importar cuántas veces se llame y que se pueda invocar cuando BeginAwait devuelva falso (según la suposición anterior)
Parecería ser un guardián contra las condiciones de carrera Si en línea las afirmaciones donde movenext es llamado por un hilo diferente después del estado = 0 en las preguntas, se verá algo como el siguiente
this.<a1>t__$await2 = this.t1.GetAwaiter<int>();
this.<>1__state = 1;
this.$__doFinallyBodies = false;
this.<a1>t__$await2.BeginAwait(this.MoveNextDelegate)
this.<>1__state = 0;
//second thread
this.<a1>t__$await2 = this.t1.GetAwaiter<int>();
this.<>1__state = 1;
this.$__doFinallyBodies = false;
this.<a1>t__$await2.BeginAwait(this.MoveNextDelegate)
this.$__doFinallyBodies = true;
this.<>1__state = 0;
this.<1>t__$await1 = this.<a1>t__$await2.EndAwait();
//other thread
this.<1>t__$await1 = this.<a1>t__$await2.EndAwait();
Si las suposiciones anteriores son correctas, hay algún trabajo innecesario, como obtener sawiater y reasignar el mismo valor a <1> t __ $ await1. Si el estado se mantuvo en 1, entonces la última parte sería:
//second thread
//I suppose this un matched call to EndAwait will fail
this.<1>t__$await1 = this.<a1>t__$await2.EndAwait();
Además, si se configurara en 2, la máquina de estados supondría que ya había obtenido el valor de la primera acción, lo que no sería cierto y una variable (potencialmente) no asignada se usaría para calcular el resultado.