remarks cref c# .net winrt-xaml async-await

cref - remarks c#



¿Por qué está esperando la llamada para completar la Tarea principal prematuramente? (2)

Estoy intentando crear un control que expone un evento DoLoading que los consumidores pueden suscribirse para realizar operaciones de carga. Para mayor comodidad, los manejadores de eventos deben ser llamados desde el subproceso UI, lo que permite a los consumidores actualizar la interfaz de usuario a voluntad, pero también podrán usar async / await para realizar tareas de larga ejecución sin bloquear el subproceso de interfaz de usuario.

Para esto, he declarado al siguiente delegado:

public delegate Task AsyncEventHandler<TEventArgs>(object sender, TEventArgs e);

Eso permite a los consumidores suscribirse al evento:

public event AsyncEventHandler<bool> DoLoading;

La idea es que los consumidores se suscriban al evento como tal (esta línea se ejecuta en el hilo de la interfaz de usuario):

loader.DoLoading += async (s, e) => { for (var i = 5; i > 0; i--) { loader.Text = i.ToString(); // UI update await Task.Delay(1000); // long-running task doesn''t block UI } };

En un punto apropiado en el tiempo, TaskScheduler un TaskScheduler para el hilo de UI y lo almacena en _uiScheduler .

El evento se desencadena cuando es apropiado por el loader con la siguiente línea (esto sucede en un hilo aleatorio):

this.PerformLoadingActionAsync().ContinueWith( _ => { // Other operations that must happen on UI thread }, _uiScheduler);

Observe que esta línea no se llama desde el subproceso UI, pero necesita actualizar la interfaz de usuario cuando se completa la carga, por lo que estoy usando ContinueWith para ejecutar el código en el planificador de tareas de la interfaz de usuario cuando se completa la tarea de carga.

Probé varias variaciones de los siguientes métodos, ninguno de los cuales funcionó, así que aquí es donde estoy:

private async Task<Task> PerformLoadingActionAsync() { TaskFactory uiFactory = new TaskFactory(_uiScheduler); // Trigger event on the UI thread and await its execution Task evenHandlerTask = await uiFactory.StartNew(async () => await this.OnDoLoading(_mustLoadPreviousRunningState)); // This can be ignored for now as it completes immediately Task commandTask = Task.Run(() => this.ExecuteCommand()); return Task.WhenAll(evenHandlerTask, commandTask); } private async Task OnDoLoading(bool mustLoadPreviousRunningState) { var handler = this.DoLoading; if (handler != null) { await handler(this, mustLoadPreviousRunningState); } }

Como puede ver, estoy comenzando dos tareas y espero que mi ContinueWith de antes ejecute una completa.

El commandTask completa inmediatamente, por lo que puede ignorarse por el momento. eventHandlerTask , como lo veo, solo debe completar uno que complete el controlador de eventos, dado que estoy esperando la llamada al método que llama al controlador de eventos y estoy esperando el controlador de eventos.

Sin embargo, lo que está sucediendo realmente es que las tareas se están completando tan pronto como la línea await Task.Delay(1000) en mi controlador de eventos.

¿Por qué es esto y cómo puedo obtener el comportamiento que espero?


En primer lugar, le recomiendo que reconsidere el diseño de su "evento asincrónico".

Es cierto que puede usar un valor de retorno de Task , pero es más natural que los manejadores de eventos C # devuelvan el void . En particular, si tiene varias suscripciones, la Task devuelta por el handler(this, ...) es solo el valor de retorno de uno de los manejadores de eventos. Para esperar correctamente a que se completen todos los eventos asincrónicos, necesitarás usar Delegate.GetInvocationList con Task.WhenAll cuando Task.WhenAll el evento.

Como ya está en la plataforma WinRT, le recomiendo que use "diferimientos". Esta es la solución elegida por el equipo de WinRT para eventos asincrónicos, por lo que debería ser familiar para los consumidores de su clase.

Desafortunadamente, el equipo de WinRT no incluyó la infraestructura de aplazamiento en el marco .NET para WinRT. Así que escribí una publicación de blog sobre los controladores de eventos asíncronos y cómo crear un administrador de aplazamiento .

Utilizando un aplazamiento, su código de generación de eventos se vería así:

private Task OnDoLoading(bool mustLoadPreviousRunningState) { var handler = this.DoLoading; if (handler == null) return; var args = new DoLoadingEventArgs(this, mustLoadPreviousRunningState); handler(args); return args.WaitForDeferralsAsync(); } private Task PerformLoadingActionAsync() { TaskFactory uiFactory = new TaskFactory(_uiScheduler); // Trigger event on the UI thread. var eventHandlerTask = uiFactory.StartNew(() => OnDoLoading(_mustLoadPreviousRunningState)).Unwrap(); Task commandTask = Task.Run(() => this.ExecuteCommand()); return Task.WhenAll(eventHandlerTask, commandTask); }

Esa es mi recomendación para una solución. Los beneficios de un aplazamiento son que habilita controladores tanto síncronos como asíncronos, es una técnica ya familiar para los desarrolladores de WinRT, y maneja correctamente múltiples suscriptores sin código adicional.

Ahora, en cuanto a por qué el código original no funciona, puede pensar esto prestando especial atención a todos los tipos en su código e identificando lo que representa cada tarea. Tenga en cuenta los siguientes puntos importantes:

  • Task<T> deriva de Task . Esto significa que la Task<Task> se convertirá en Task sin advertencias.
  • StartNew no es async por lo que se comporta de manera diferente a Task.Run . Vea la excelente publicación de blog de Stephen Toub sobre el tema .

Su método OnDoLoading devolverá una Task representa la finalización del último controlador de eventos. Se ignoran todas las Task de otros manejadores de eventos (como mencioné anteriormente, debe usar Delegate.GetInvocationList o diferimientos para admitir correctamente múltiples manejadores asíncronos).

Ahora veamos PerformLoadingActionAsync :

Task evenHandlerTask = await uiFactory.StartNew(async () => await this.OnDoLoading(_mustLoadPreviousRunningState));

Están pasando muchas cosas en esta declaración. Es semánticamente equivalente a esta línea de código (un poco más simple):

Task evenHandlerTask = await uiFactory.StartNew(() => OnDoLoading(_mustLoadPreviousRunningState));

OK, entonces estamos haciendo cola OnDoLoading al hilo de UI. El tipo de OnDoLoading de OnDoLoading es Task , por lo que el tipo de StartNew de StartNew es Task<Task> . El blog de Stephen Toub analiza los detalles de este tipo de envoltura , pero puedes pensarlo de esta manera: la tarea "externa" representa el inicio del método OnDoLoading asíncrono (hasta que tenga que ceder en await ), y el " la tarea "interna" representa la finalización del método OnDoLoading asíncrono.

A continuación, await el resultado de StartNew . Esto desenvuelve la tarea "externa" y obtenemos una Task que representa la finalización de OnDoLoading almacenado en evenHandlerTask .

return Task.WhenAll(evenHandlerTask, commandTask);

Ahora está devolviendo una Task que representa cuando se han completado tanto evenHandlerTask como evenHandlerTask . Sin embargo, está en un método async , por lo que su tipo de devolución real es Task<Task> , y es la tarea interna lo que representa lo que desea. Creo que lo que querías hacer era:

await Task.WhenAll(evenHandlerTask, commandTask);

Lo cual le daría un tipo de Task de devolución, que representa la finalización completa.

Si miras cómo se llama:

this.PerformLoadingActionAsync().ContinueWith(...)

ContinueWith está actuando en la Task externa en el código original, cuando realmente quería que actuara en la Task interna .


Se dio cuenta correctamente de que StartNew() devuelve Task<Task> en este caso, y le importa la Task interna (aunque no estoy seguro de por qué está esperando la Task externa antes de iniciar commandTask ).

Pero luego devuelve Task<Task> e ignora la Task interna. Lo que debe hacer es usar await lugar de return y cambiar el tipo de retorno de PerformLoadingActionAsync() a Just Task :

await Task.WhenAll(evenHandlerTask, commandTask);

Pocas notas más:

  1. Usar manejadores de eventos de esta manera es bastante peligroso, porque te importa la Task devuelta por el controlador, pero si hay más controladores, solo se devolverá la última Task si planteas el evento normalmente. Si realmente desea hacer esto, debe llamar a GetInvocationList() , que le permite invocar y await cada manejador por separado:

    private async Task OnDoLoading(bool mustLoadPreviousRunningState) { var handler = this.DoLoading; if (handler != null) { var handlers = handler.GetInvocationList(); foreach (AsyncEventHandler<bool> innerHandler in handlers) { await innerHandler(this, mustLoadPreviousRunningState); } } }

    Si sabe que nunca tendrá más de un controlador, podría usar una propiedad de delegado que se pueda establecer directamente en lugar de un evento.

  2. Si tiene un método async o lambda que tiene el único que await justo antes de su return (y finally no s), entonces no necesita hacerlo async , solo devuelva la Task directamente:

    Task.Factory.StartNew(() => this.OnDoLoading(true))