web-dev-qa-db-fra.com

Quand utiliser correctement Task.Run et quand async-wait

J'aimerais vous demander votre opinion sur l'architecture correcte quand utiliser Task.Run. Je rencontre une interface utilisateur lente dans notre application WPF .NET 4.5 (avec le framework Caliburn Micro).

En gros, je suis en train de faire (des extraits de code très simplifiés):

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();
    }
}

D'après les articles/vidéos que j'ai lus/vus, je sais que awaitasync ne fonctionne pas nécessairement sur un fil d'arrière-plan et pour commencer à travailler en arrière-plan, vous devez l'envelopper avec wait Task.Run(async () => ... ) . L'utilisation de asyncawait ne bloque pas l'interface utilisateur, mais continue de s'exécuter sur le thread d'interface utilisateur, ce qui la rend lente.

Où est le meilleur endroit pour mettre Task.Run?

Devrais-je juste

  1. Enveloppez l'appel externe car il s'agit d'un travail de threading moins important pour .NET

  2. , ou devrais-je envelopper uniquement les méthodes liées au CPU s'exécutant en interne avec Task.Run car cela le rend réutilisable pour d'autres endroits? Je ne suis pas sûr de savoir si commencer le travail sur les fils d’arrière-plan en profondeur est une bonne idée.

Ad (1), la première solution serait la suivante:

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.

Ad (2), la deuxième solution serait la suivante:

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
}
278
Lukas K

Notez le instructions pour effectuer du travail sur un thread d'interface utilisateur , collecté sur mon blog:

  • Ne bloquez pas le thread d'interface utilisateur pendant plus de 50 ms à la fois.
  • Vous pouvez planifier environ 100 continuations sur le fil de l'interface utilisateur par seconde; 1000 c'est trop.

Vous devez utiliser deux techniques:

1) Utilisez ConfigureAwait(false) lorsque vous le pouvez.

Par exemple, await MyAsync().ConfigureAwait(false); au lieu de await MyAsync();.

ConfigureAwait(false) indique à la await qu'il n'est pas nécessaire de reprendre le contexte actuel (dans ce cas, "sur le contexte actuel" signifie "sur le fil de l'interface utilisateur"). Cependant, pour le reste de cette méthode async (après la ConfigureAwait), vous ne pouvez rien faire si vous supposez être dans le contexte actuel (par exemple, mettre à jour des éléments de l'interface utilisateur).

Pour plus d'informations, consultez mon article MSDN Meilleures pratiques en programmation asynchrone .

2) Utilisez Task.Run pour appeler des méthodes liées à la CPU.

Vous devez utiliser Task.Run, mais pas dans les codes que vous voulez réutiliser (code de bibliothèque, par exemple). Donc, vous utilisez Task.Run pour appeler la méthode, pas dans le cadre de l'implémentation de la méthode.

Donc, le travail purement lié au processeur ressemblerait à ceci:

// Documentation: This method is CPU-bound.
void DoWork();

Ce que vous appelleriez avec Task.Run:

await Task.Run(() => DoWork());

Les méthodes qui sont un mélange de CPU et d’E/S doivent avoir une signature Async avec une documentation indiquant leur nature liée à la CPU:

// Documentation: This method is CPU-bound.
Task DoWorkAsync();

Vous pouvez également appeler avec Task.Run (car il est partiellement lié au processeur):

await Task.Run(() => DoWorkAsync());
328
Stephen Cleary

Un problème avec votre ContentLoader est qu’en interne il fonctionne séquentiellement. Un meilleur modèle consiste à paralléliser le travail et ensuite à synchroniser à la fin, de sorte que nous obtenons

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);
    }
}

Évidemment, cela ne fonctionne pas si l'une des tâches nécessite des données provenant d'autres tâches précédentes, mais cela devrait vous permettre d'améliorer le débit global dans la plupart des scénarios.

11
Paul Hatcher