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 deTask
. Esto significa que laTask<Task>
se convertirá enTask
sin advertencias. -
StartNew
no esasync
por lo que se comporta de manera diferente aTask.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:
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 últimaTask
si planteas el evento normalmente. Si realmente desea hacer esto, debe llamar aGetInvocationList()
, que le permite invocar yawait
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.
Si tiene un método
async
o lambda que tiene el único queawait
justo antes de sureturn
(yfinally
no s), entonces no necesita hacerloasync
, solo devuelva laTask
directamente:Task.Factory.StartNew(() => this.OnDoLoading(true))