c# - method - Cuando se usa correctamente Task.Run y cuando solo async-await
task c# (2)
Me gustaría preguntarle su opinión acerca de la arquitectura correcta cuando usar Task.Run
. Estoy experimentando la interfaz de usuario laggy en nuestra aplicación WPF .NET 4.5 (con Caliburn Micro Framework).
Básicamente 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 se ejecuta necesariamente en un subproceso en segundo plano y para comenzar a trabajar en segundo plano es necesario envolver con Task.Run(async () => ... )
. El uso de async
await
no bloquea la interfaz de usuario, pero aún se está ejecutando en el subproceso de la interfaz de usuario, por lo que está retrasado.
¿Dónde está el mejor lugar para poner Task.Run?
¿Debería solo
Ajustar la llamada externa porque esto es menos trabajo de subprocesos para .NET
¿O debería envolver solo los métodos vinculados a la CPU que se ejecutan internamente con
Task.Run
ya que esto hace que sea reutilizable para otros lugares? No estoy seguro si es una buena idea comenzar a trabajar en subprocesos de fondo en el núcleo.
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 subproceso de la interfaz de usuario , recopilados en mi blog:
- No bloquee el hilo de la interfaz de usuario por más de 50 ms a la vez.
- Puede programar ~ 100 continuaciones en el hilo de la IU por segundo; 1000 es demasiado
Hay dos técnicas que debes usar:
1) Use ConfigureAwait(false)
cuando pueda.
Por ejemplo, await MyAsync().ConfigureAwait(false);
en lugar de await MyAsync();
.
ConfigureAwait(false)
le dice a la await
que no necesita reanudar en el contexto actual (en este caso, "en el contexto actual" significa "en el subproceso de la interfaz de usuario"). Sin embargo, para el resto de ese método async
(después de ConfigureAwait
), no puede hacer nada que asuma que está 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 asíncrona .
2) Use Task.Run
para llamar a métodos enlazados a la CPU.
Debe usar Task.Run
, pero no dentro de ningún código que desee reutilizar (es decir, código de biblioteca). Así que usa Task.Run
para llamar al método, no como parte de la implementación del método.
Así que el trabajo puramente vinculado a la CPU se vería así:
// Documentation: This method is CPU-bound.
void DoWork();
A la que llamarías usando Task.Run
:
await Task.Run(() => DoWork());
Los métodos que son una mezcla de CPU enlazados y E / S enlazados deben tener una firma Async
con documentación que indique su naturaleza vinculada a la CPU:
// Documentation: This method is CPU-bound.
Task DoWorkAsync();
A lo que también llamaría usando Task.Run
(ya que está parcialmente vinculado a la CPU):
await Task.Run(() => DoWorkAsync());
Un problema con su ContentLoader es que internamente opera de manera 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 proporcionarle un mejor rendimiento general para la mayoría de los escenarios.