Cuando se usa correctamente Task.Run y cuando solo se espera async
Me gustaría pedirle su opinión sobre la arquitectura correcta y cuándo utilizarla Task.Run
. Estoy experimentando retrasos en la interfaz de usuario en nuestra aplicación WPF .NET 4.5 (con Caliburn Micro framework).
Básicamente lo que estoy haciendo (fragmentos de código muy simplificados):
public class PageViewModel : IHandle<SomeMessage>
{
...
public async void Handle(SomeMessage message)
{
ShowLoadingAnimation();
// Makes UI very laggy, but still not dead
await this.contentLoader.LoadContentAsync();
HideLoadingAnimation();
}
}
public class ContentLoader
{
public async Task LoadContentAsync()
{
await DoCpuBoundWorkAsync();
await DoIoBoundWorkAsync();
await DoCpuBoundWorkAsync();
// I am not really sure what all I can consider as CPU bound as slowing down the UI
await DoSomeOtherWorkAsync();
}
}
Por los artículos/videos que leí/vi, sé que await
async
no necesariamente se ejecuta en un hilo en segundo plano y que para comenzar a trabajar en segundo plano es necesario cerrarlo con await Task.Run(async () => ... )
. El uso async
await
no bloquea la interfaz de usuario, pero aún se ejecuta en el subproceso de la interfaz de usuario, por lo que se retrasa.
¿Cuál es el mejor lugar para colocar Task.Run?
¿Debería simplemente
Ajuste la llamada externa porque esto requiere menos trabajo de subprocesamiento para .NET
, ¿o debería incluir solo los métodos vinculados a la CPU que se ejecutan internamente,
Task.Run
ya que esto los hace reutilizables para otros lugares? No estoy seguro de si comenzar a trabajar en subprocesos en segundo plano en lo más profundo del núcleo es una buena idea.
Anuncio (1), la primera solución sería así:
public async void Handle(SomeMessage message)
{
ShowLoadingAnimation();
await Task.Run(async () => await this.contentLoader.LoadContentAsync());
HideLoadingAnimation();
}
// Other methods do not use Task.Run as everything regardless
// if I/O or CPU bound would now run in the background.
Anuncio (2), la segunda solución sería así:
public async Task DoCpuBoundWorkAsync()
{
await Task.Run(() => {
// Do lot of work here
});
}
public async Task DoSomeOtherWorkAsync(
{
// I am not sure how to handle this methods -
// probably need to test one by one, if it is slowing down UI
}
Tenga en cuenta las pautas para realizar trabajos en un hilo de interfaz de usuario , recopiladas en mi blog:
- No bloquee el hilo de la interfaz de usuario durante más de 50 ms a la vez.
- Puede programar ~100 continuaciones en el subproceso de la interfaz de usuario por segundo; 1000 es demasiado.
Hay dos técnicas que debes utilizar:
1) Úsalo ConfigureAwait(false)
cuando puedas.
Por ejemplo, await MyAsync().ConfigureAwait(false);
en lugar de await MyAsync();
.
ConfigureAwait(false)
le indica await
que no es necesario continuar en el contexto actual (en este caso, "en el contexto actual" significa "en el hilo de la interfaz de usuario"). Sin embargo, durante el resto de ese async
método (después de ConfigureAwait
), no puedes hacer nada que suponga que estás en el contexto actual (por ejemplo, actualizar elementos de la interfaz de usuario).
Para obtener más información, consulte mi artículo de MSDN Mejores prácticas en programación asincrónica .
2) Úselo Task.Run
para llamar a métodos vinculados a la CPU.
Debe utilizar Task.Run
, pero no dentro de ningún código que desee que sea reutilizable (es decir, código de biblioteca). Por lo tanto, se utiliza Task.Run
para llamar al método, no como parte de la implementación del método.
Entonces, el trabajo puramente vinculado a la CPU se vería así:
// Documentation: This method is CPU-bound.
void DoWork();
Al que llamarías usando Task.Run
:
await Task.Run(() => DoWork());
Los métodos que son una combinación de CPU y E/S deben tener una Async
firma con documentación que indique su naturaleza vinculada a la CPU:
// Documentation: This method is CPU-bound.
Task DoWorkAsync();
Lo que también llamarías usando Task.Run
(ya que está parcialmente vinculado a la CPU):
await Task.Run(() => DoWorkAsync());
Un problema con ContentLoader es que internamente funciona de forma secuencial. Un mejor patrón es paralelizar el trabajo y luego sincronizarlo al final, así obtenemos
public class PageViewModel : IHandle<SomeMessage>
{
...
public async void Handle(SomeMessage message)
{
ShowLoadingAnimation();
// makes UI very laggy, but still not dead
await this.contentLoader.LoadContentAsync();
HideLoadingAnimation();
}
}
public class ContentLoader
{
public async Task LoadContentAsync()
{
var tasks = new List<Task>();
tasks.Add(DoCpuBoundWorkAsync());
tasks.Add(DoIoBoundWorkAsync());
tasks.Add(DoCpuBoundWorkAsync());
tasks.Add(DoSomeOtherWorkAsync());
await Task.WhenAll(tasks).ConfigureAwait(false);
}
}
Obviamente, esto no funciona si alguna de las tareas requiere datos de otras tareas anteriores, pero debería brindarle un mejor rendimiento general en la mayoría de los escenarios.