c# .net c#-4.0 async-ctp

c# - Async CTP y "finalmente"



.net c#-4.0 (2)

Aquí está el código:

static class AsyncFinally { static async Task<int> Func( int n ) { try { Console.WriteLine( " Func: Begin #{0}", n ); await TaskEx.Delay( 100 ); Console.WriteLine( " Func: End #{0}", n ); return 0; } finally { Console.WriteLine( " Func: Finally #{0}", n ); } } static async Task Consumer() { for ( int i = 1; i <= 2; i++ ) { Console.WriteLine( "Consumer: before await #{0}", i ); int u = await Func( i ); Console.WriteLine( "Consumer: after await #{0}", i ); } Console.WriteLine( "Consumer: after the loop" ); } public static void AsyncTest() { Task t = TaskEx.RunEx( Consumer ); t.Wait(); Console.WriteLine( "After the wait" ); } }

Aquí está la salida:

Consumer: before await #1 Func: Begin #1 Func: End #1 Consumer: after await #1 Consumer: before await #2 Func: Begin #2 Func: Finally #1 Func: End #2 Consumer: after await #2 Consumer: after the loop Func: Finally #2 After the wait

Como puedes ver, el bloque finally se ejecuta mucho más tarde de lo que esperas.

¿Alguna solución?

¡Gracias por adelantado!


Editar

Por favor considere la answer Theo Yaung.

Respuesta original

No estoy familiarizado con async / await, pero después de leer esto: Descripción general de Visual Studio Async CTP

y al leer su código, veo la await en la función Func(int n) , lo que significa que desde el código después de la palabra clave await hasta el final de la función se ejecutará más adelante como delegado.

Así que mi conjetura (y esta es una conjetura sin educación) es que Func:Begin y Func:End posiblemente se ejecutarán en diferentes "contextos" (¿hilos?), Es decir, de forma asíncrona.

Por lo tanto, int u = await Func( i ); La línea en Consumer continuará su ejecución en el momento en que se alcance el código que await en Func . Entonces es bastante posible tener:

Consumer: before await #1 Func: Begin #1 Consumer: after await #1 Consumer: before await #2 Func: Begin #2 Consumer: after await #2 Consumer: after the loop Func: End #1 // Can appear at any moment AFTER "after await #1" // but before "After the wait" Func: Finally #1 // will be AFTER "End #1" but before "After the wait" Func: End #2 // Can appear at any moment AFTER "after await #2" // but before "After the wait" Func: Finally #2 // will be AFTER "End #2" but before "After the wait" After the wait // will appear AFTER the end of all the Tasks

El Func: End y el Func: Finally pueden aparecer en cualquier posición en los registros, la única restricción es que un Func: End #X aparecerá antes de su Func: Finally #X asociado Func: Finally #X , y que ambos deben aparecer antes del After the wait .

Como lo explica Henk Holterman (de forma un tanto abrupta), es que el hecho de que usted await en el cuerpo de Func significa que todo lo que se ejecute después será ejecutado algunas veces después.

No hay una solución alternativa, ya que, by design usted await entre el Begin y el End de Func .

Sólo mis 2 eurocents sin educación.


Esta es una excelente captura, y estoy de acuerdo en que hay un error en el CTP aquí. Me metí en esto y aquí está lo que está pasando:

Esta es una combinación de la implementación CTP de las transformaciones del compilador asíncrono, así como el comportamiento existente de la TPL (Task Parallel Library) de .NET 4.0+. Aquí están los factores en juego:

  1. El cuerpo finalmente de la fuente se traduce en parte de un cuerpo CLR real. Esto es deseable por muchas razones, una de las cuales es que podemos hacer que el CLR lo ejecute sin capturar / volver a emitir la excepción un tiempo adicional. Esto también simplifica hasta cierto punto nuestro código gen. El código gen más simple resulta en binarios más pequeños una vez compilados, lo que definitivamente es deseado por muchos de nuestros clientes. :)
  2. La Task general para el método Func(int n) es una tarea TPL real. Cuando await en Consumer() , el resto del método Consumer() se instala realmente como una continuación de la finalización de la Task devuelta desde Func(int n) .
  3. La forma en que el compilador CTP transforma los métodos asíncronos hace que un return se SetResult(...) a una SetResult(...) antes de un retorno real. SetResult(...) reduce a una llamada a TaskCompletionSource<>.TrySetResult .
  4. TaskCompletionSource<>.TrySetResult señala la finalización de la tarea TPL. Al instante permitiendo que sus continuaciones ocurran "en algún momento". Este "en algún momento" puede significar en otro hilo, o en algunas condiciones, el TPL es inteligente y dice "um, yo también podría llamarlo ahora en este mismo hilo".
  5. La Task general para Func(int n) vuelve técnicamente "Completada" justo antes de que finalmente se ejecute. Esto significa que el código que estaba esperando en un método asíncrono puede ejecutarse en subprocesos paralelos, o incluso antes del bloque final.

Teniendo en cuenta que se supone que la Task general representa el estado asíncrono del método, fundamentalmente no debería marcarse como completado hasta que al menos todo el código provisto por el usuario se haya ejecutado según el diseño del idioma. Trataré esto con Anders, el equipo de diseño de idiomas y los desarrolladores de compiladores para que esto se vea.

Ámbito de la manifestación / severidad:

Por lo general, no se verá afectado por esto tan mal en un caso de WPF o WinForms en el que tiene algún tipo de bucle de mensajes administrados en marcha. El motivo es que las implementaciones de la await en la Task remiten al SynchronizationContext . Esto hace que las continuaciones asíncronas se pongan en cola en el bucle de mensaje preexistente para ejecutarse en el mismo hilo. Puede verificar esto cambiando su código para ejecutar Consumer() de la siguiente manera:

DispatcherFrame frame = new DispatcherFrame(exitWhenRequested: true); Action asyncAction = async () => { await Consumer(); frame.Continue = false; }; Dispatcher.CurrentDispatcher.BeginInvoke(asyncAction); Dispatcher.PushFrame(frame);

Una vez ejecutado dentro del contexto del bucle de mensajes de WPF, la salida aparece como se esperaría:

Consumer: before await #1 Func: Begin #1 Func: End #1 Func: Finally #1 Consumer: after await #1 Consumer: before await #2 Func: Begin #2 Func: End #2 Func: Finally #2 Consumer: after await #2 Consumer: after the loop After the wait

Solución:

Por desgracia, la solución significa cambiar su código para que no use declaraciones de return dentro de un bloque try/finally . Sé que esto realmente significa que pierdes mucha elegancia en el flujo de tu código. Puede utilizar métodos de ayuda asíncronos o lambdas de ayuda para solucionar este problema. Personalmente, prefiero el helper-lambdas porque se cierra automáticamente a través de los locales / parámetros del método que lo contiene, así como también mantiene el código relevante más cerca.

Ayudante de enfoque lambda:

static async Task<int> Func( int n ) { int result; try { Func<Task<int>> helperLambda = async() => { Console.WriteLine( " Func: Begin #{0}", n ); await TaskEx.Delay( 100 ); Console.WriteLine( " Func: End #{0}", n ); return 0; }; result = await helperLambda(); } finally { Console.WriteLine( " Func: Finally #{0}", n ); } // since Func(...)''s return statement is outside the try/finally, // the finally body is certain to execute first, even in face of this bug. return result; }

Método de ayuda:

static async Task<int> Func(int n) { int result; try { result = await HelperMethod(n); } finally { Console.WriteLine(" Func: Finally #{0}", n); } // since Func(...)''s return statement is outside the try/finally, // the finally body is certain to execute first, even in face of this bug. return result; } static async Task<int> HelperMethod(int n) { Console.WriteLine(" Func: Begin #{0}", n); await TaskEx.Delay(100); Console.WriteLine(" Func: End #{0}", n); return 0; }

Como un enchufe descarado: estamos contratando en el espacio de idiomas en Microsoft y siempre estamos buscando un gran talento. Entrada de blog here con la lista completa de posiciones abiertas :)