Lorsque vous utilisez correctement Task.Run et lorsque vous attendez simplement async

318

Je voudrais vous demander votre avis sur la bonne architecture à utiliser Task.Run. J'expérimente une interface utilisateur laggy dans notre application WPF .NET 4.5 (avec le framework Caliburn Micro).

Fondamentalement, je fais (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 cela await asyncne fonctionne pas nécessairement sur un fil d'arrière-plan et pour commencer à travailler en arrière-plan, vous devez l'envelopper avec attendre Task.Run(async () => ... ). L'utilisation async awaitne bloque pas l'interface utilisateur, mais elle fonctionne toujours sur le thread d'interface utilisateur, ce qui la rend retardée.

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

Dois-je juste

  1. Encapsulez l'appel externe car cela représente moins de travail de threading pour .NET

  2. , ou dois-je envelopper uniquement les méthodes liées au processeur qui s'exécutent en interne, Task.Runcar cela les rend réutilisables pour d'autres endroits? Je ne sais pas ici si commencer à travailler sur des threads d'arrière-plan profondément enfouis 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
}
Lukas K
la source
BTW, la ligne en (1) await Task.Run(async () => await this.contentLoader.LoadContentAsync());devrait simplement être await Task.Run( () => this.contentLoader.LoadContentAsync() );. AFAIK vous ne gagnez rien en ajoutant une seconde awaitet à l' asyncintérieur Task.Run. Et puisque vous ne passez pas de paramètres, cela simplifie un peu plus await Task.Run( this.contentLoader.LoadContentAsync );.
ToolmakerSteve
il y a en fait une petite différence si vous avez une seconde attente à l'intérieur. Consultez cet article . Je l'ai trouvé très utile, juste avec ce point particulier, je ne suis pas d'accord et je préfère retourner directement Task au lieu d'attendre. (comme vous le suggérez dans votre commentaire)
Lukas K

Réponses:

365

Noter la directives pour effectuer un travail sur un thread d'interface utilisateur , collectées sur mon blog:

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

Vous devez utiliser deux techniques:

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

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

ConfigureAwait(false)indique awaitque vous n'avez pas besoin de reprendre sur le contexte actuel (dans ce cas, "sur le contexte actuel" signifie "sur le thread d'interface utilisateur"). Cependant, pour le reste de cette asyncméthode (après le ConfigureAwait), vous ne pouvez rien faire qui suppose que vous êtes dans le contexte actuel (par exemple, mettre à jour les éléments de l'interface utilisateur).

Pour plus d'informations, consultez mon article MSDN Best Practices in Asynchronous Programming .

2) Utilisez Task.Runpour appeler des méthodes liées au CPU.

Vous devez utiliser Task.Run, mais pas dans le code que vous souhaitez être réutilisable (c'est-à-dire, le code de bibliothèque). Vous utilisez donc Task.Runpour appeler la méthode, pas dans le cadre de la mise en œuvre de la méthode.

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

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

Que vous appelleriez en utilisant Task.Run:

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

Les méthodes qui sont un mélange de lié au processeur et lié aux E / S doivent avoir une Asyncsignature avec une documentation soulignant leur nature liée au processeur:

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

Ce que vous appelleriez également en utilisant Task.Run(car il est partiellement lié au CPU):

await Task.Run(() => DoWorkAsync());
Stephen Cleary
la source
4
Merci pour votre réponse rapide! Je connais le lien que vous avez publié et vu les vidéos référencées dans votre blog. En fait, c'est pourquoi j'ai posté cette question - dans la vidéo est dit (comme dans votre réponse), vous ne devriez pas utiliser Task.Run dans le code de base. Mais mon problème est que j'ai besoin d'envelopper une telle méthode à chaque fois que je l'utilise pour ne pas ralentir les responsivnes (veuillez noter que tout mon code est asynchrone et ne bloque pas, mais sans Thread.Run c'est juste laggy). Je ne sais pas non plus s'il est préférable de simplement envelopper les méthodes liées au processeur (de nombreux appels Task.Run) ou de tout en un dans Task.Run?
Lukas K
12
Toutes vos méthodes de bibliothèque devraient être utilisées ConfigureAwait(false). Si vous le faites en premier, vous constaterez peut-être que cela Task.Runest complètement inutile. Si vous ne toujours besoin Task.Run, alors il ne fait pas beaucoup de différence à l'exécution dans ce cas si vous appelez une ou plusieurs fois, alors faites tout ce qui est le plus naturel pour votre code.
Stephen Cleary
2
Je ne comprends pas comment la première technique l'aidera. Même si vous utilisez ConfigureAwait(false)votre méthode liée au processeur, c'est toujours le thread d'interface utilisateur qui va faire la méthode liée au processeur, et seul tout ce qui suit peut être effectué sur un thread TP. Ou ai-je mal compris quelque chose?
Darius
4
@ user4205580: Non, Task.Runcomprend les signatures asynchrones, donc il ne se terminera pas avant la DoWorkAsyncfin. Le supplément async/ awaitn'est pas nécessaire. J'explique davantage le «pourquoi» dans ma série de blogs sur l' Task.Runétiquette .
Stephen Cleary
3
@ user4205580: Non. La grande majorité des méthodes asynchrones "de base" ne l'utilisent pas en interne. La manière normale d'implémenter une méthode asynchrone «de base» consiste à utiliser l' TaskCompletionSource<T>une de ses notations abrégées telles que FromAsync. J'ai un article de blog qui explique plus en détail pourquoi les méthodes asynchrones ne nécessitent pas de threads .
Stephen Cleary
12

Un problème avec votre ContentLoader est qu'il fonctionne en interne de manière séquentielle. Un meilleur modèle consiste à paralléliser le travail, puis à se synchroniser à la fin, donc 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);
    }
}

De toute évidence, cela ne fonctionne pas si l'une des tâches nécessite des données d'autres tâches antérieures, mais devrait vous donner un meilleur débit global pour la plupart des scénarios.

Paul Hatcher
la source