Ayer estaba dando una charla sobre la nueva función "asíncrona" de C #, en particular profundizando en el aspecto del código generado y the GetAwaiter()
/ BeginAwait()
/ EndAwait()
llamadas.
Observamos con cierto detalle la máquina de estado generada por el compilador de C #, y había dos aspectos que no podíamos entender:
- Por qué la clase generada contiene un
Dispose()
método y una$__disposing
variable, que nunca parecen usarse (y la clase no se implementaIDisposable
). - Por qué la
state
variable interna se establece en 0 antes de cualquier llamada aEndAwait()
, cuando 0 normalmente parece significar "este es el punto de entrada inicial".
Sospecho que el primer punto podría responderse haciendo algo más interesante dentro del método asíncrono, 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 MoveNext()
método que implementa la máquina de estados. Esto se copia directamente desde Reflector. No he arreglado los nombres de 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 cambia nuevamente antes de que se observe de manera obvia ... entonces, ¿por qué establecerlo en 0? Si MoveNext()
se volviera a llamar en este punto (ya sea directamente o por medio Dispose
), volvería a iniciar efectivamente el método asíncrono, lo que sería totalmente inapropiado por lo que puedo decir ... si MoveNext()
se llama y no se llama, el cambio de estado es irrelevante.
¿Es esto simplemente un efecto secundario de que el compilador reutiliza el código de generación de bloque de iterador para asíncrono, donde puede tener una explicación más obvia?
Descargo de responsabilidad importante
Obviamente esto es solo un compilador CTP. Espero 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 trata de afirmar que esto es una falla en el compilador de C # o algo así. Solo estoy tratando de averiguar si hay una razón sutil para esto que me haya perdido :)
fuente
Respuestas:
Bien, 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 confirmó 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 es aquí sólo es especial porque es no un estado válido, que es posible que en justo antes de la
await
en un caso normal. En particular, no es un estado que la máquina de estado pueda 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 adicional al estado 0 en este momento, pero en última instancia, realmente no importa. El punto de esta pregunta era descubrir por qué se está estableciendo el estado.El valor es relevante si la espera finaliza en una excepción que se detecta. Podemos terminar volviendo a la misma declaración de espera nuevamente, pero no debemos estar en el estado que significa "Estoy a punto de volver de esa espera", ya que de lo contrario se omitiría todo tipo 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.
Aquí está el método asíncrono:
Conceptualmente,
SimpleAwaitable
puede ser cualquier espera, tal vez una tarea, tal vez otra cosa. A los fines de mis pruebas, siempre devuelve false paraIsCompleted
y arroja una excepciónGetResult
.Aquí está el código generado para
MoveNext
:Tuve que moverme
Label_ContinuationPoint
para que sea un código válido; de lo contrario, no está en el alcance de lagoto
declaración, pero eso no afecta la respuesta.Piensa en lo que sucede cuando
GetResult
lanza su excepción. Pasaremos por el bloque de captura, incrementaremosi
y luego volveremos a dar la vuelta (suponiendoi
que todavía sea menor que 3). Todavía estamos en el estado que teníamos antes de laGetResult
llamada ... pero cuando entramos en eltry
bloque debemos imprimir "In Try" yGetAwaiter
volver a llamar ... y solo haremos eso si el estado no es 1. Sin elstate = 0
asignación, usará al camarero existente y saltará laConsole.WriteLine
llamada.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 :)
fuente
si se mantuvo en 1 (primer caso) recibiría una llamada
EndAwait
sin una llamada aBeginAwait
. Si se mantiene en 2 (segundo caso) obtendría el mismo resultado solo en el otro camarero.Supongo que llamar a BeginAwait devuelve falso si ya se ha iniciado (una suposición desde mi lado) y mantiene el valor original para devolver en EndAwait. Si ese es el caso, funcionaría correctamente, mientras que si lo configura en -1, podría tener un no inicializado
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á falso en esos casos. Comenzar, por supuesto, sería inaceptable ya que podría tener efectos secundarios o simplemente dar un resultado diferente. También se supone que EndAwaiter siempre devolverá el mismo valor sin importar cuántas veces se llame y que se puede llamar cuando BeginAwait devuelve falso (según el supuesto anterior)
Parecería ser una protección contra las condiciones de la carrera Si alineamos las declaraciones donde movenext es llamado por un hilo diferente después del estado = 0 en las preguntas, se vería algo así como el siguiente
Si las suposiciones anteriores son correctas, se realiza un trabajo innecesario, como obtener sawiater y reasignar el mismo valor a <1> t __ $ await1. Si el estado se mantuviera en 1, la última parte sería:
Además, si se estableció en 2, la máquina de estado supondría que ya había obtenido el valor de la primera acción que sería falso y que una variable (potencialmente) no asignada se utilizaría para calcular el resultado
fuente
¿Podría tener algo que ver con las llamadas asíncronas apiladas / anidadas? ..
es decir:
¿Se llama al delegado movenext varias veces en esta situación?
¿Solo un despeje?
fuente
MoveNext()
sería llamado una vez en cada uno de ellos.Explicación de los estados actuales:
posibles estados:
¿Es posible que esta implementación solo quiera asegurar que si ocurre otro Llamado a MoveNext desde donde sea (mientras espera) reevaluará nuevamente toda la cadena de estado desde el principio, para reevaluar resultados que, mientras tanto, podrían estar desactualizados?
fuente