Cuando se usa correctamente Task.Run y ​​cuando solo se espera async

Resuelto Lukas K asked hace 11 años • 2 respuestas

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 asyncno 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 awaitno 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

  1. Ajuste la llamada externa porque esto requiere menos trabajo de subprocesamiento para .NET

  2. , ¿o debería incluir solo los métodos vinculados a la CPU que se ejecutan internamente, Task.Runya 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
}
Lukas K avatar Aug 02 '13 16:08 Lukas K
Aceptado

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 awaitque 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 asyncmé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.Runpara 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.Runpara 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 Asyncfirma 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());
Stephen Cleary avatar Aug 02 '2013 11:08 Stephen Cleary

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.

Paul Hatcher avatar Jul 02 '2017 11:07 Paul Hatcher