c# asynchronous c#-5.0

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 implementa IDisposable ).
  • Por qué la variable de state interno se establece en 0 antes de cualquier llamada a EndAwait() , 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.