Quando usi correttamente Task.Run e quando async-waitit


317

Vorrei chiederti a tuo parere sull'architettura corretta quando utilizzarla Task.Run. Sto riscontrando un'interfaccia utente ritardata nella nostra applicazione WPF .NET 4.5 (con framework Caliburn Micro).

Fondamentalmente lo sto facendo (frammenti di codice molto semplificati):

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

Dagli articoli / video che ho letto / visto, so che await asyncnon è necessariamente in esecuzione su un thread in background e per iniziare a lavorare in background devi avvolgerlo con waitit Task.Run(async () => ... ). L'utilizzo async awaitnon blocca l'interfaccia utente, ma è ancora in esecuzione sul thread dell'interfaccia utente, quindi è in ritardo.

Qual è il posto migliore dove posizionare Task.Run?

Dovrei solo

  1. Termina la chiamata esterna perché si tratta di un lavoro di threading inferiore per .NET

  2. o dovrei avvolgere solo i metodi associati alla CPU internamente in esecuzione in Task.Runquanto ciò lo rende riutilizzabile per altri luoghi? Non sono sicuro qui se iniziare il lavoro sui thread di sfondo in profondità nel core sia una buona idea.

Annuncio (1), la prima soluzione sarebbe così:

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.

Annuncio (2), la seconda soluzione sarebbe così:

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
}

A proposito, la linea in (1) await Task.Run(async () => await this.contentLoader.LoadContentAsync());dovrebbe essere semplicemente await Task.Run( () => this.contentLoader.LoadContentAsync() );. AFAIK non guadagni nulla aggiungendo un secondo awaite asyncdentro Task.Run. E poiché non stai passando parametri, questo semplifica leggermente di più await Task.Run( this.contentLoader.LoadContentAsync );.
ToolmakerSteve

c'è in realtà una piccola differenza se hai un secondo aspetto all'interno. Vedere questo articolo . L'ho trovato molto utile, solo con questo particolare punto non sono d'accordo e preferisco restituire direttamente l'attività invece di aspettare. (come suggerisci nel tuo commento)
Lukas K

Risposte:


365

Nota le linee guida per l'esecuzione del lavoro su un thread dell'interfaccia utente , raccolte sul mio blog:

  • Non bloccare il thread dell'interfaccia utente per più di 50 ms alla volta.
  • È possibile pianificare ~ 100 continuazioni sul thread dell'interfaccia utente al secondo; 1000 è troppo.

Esistono due tecniche da utilizzare:

1) Utilizzare ConfigureAwait(false)quando è possibile.

Ad esempio, await MyAsync().ConfigureAwait(false);invece di await MyAsync();.

ConfigureAwait(false)indica awaitche non è necessario riprendere nel contesto corrente (in questo caso, "nel contesto corrente" significa "sul thread dell'interfaccia utente"). Tuttavia, per il resto di quel asyncmetodo (dopo il ConfigureAwait), non puoi fare nulla che presupponga di trovarti nel contesto corrente (ad es. Aggiornare gli elementi dell'interfaccia utente).

Per ulteriori informazioni, consultare il mio articolo MSDN Best practice in Asynchronous Programming .

2) Utilizzare Task.Runper chiamare metodi associati alla CPU.

È necessario utilizzare Task.Run, ma non all'interno di qualsiasi codice che si desidera sia riutilizzabile (ad esempio, il codice della libreria). Quindi usi Task.Runper chiamare il metodo, non come parte dell'implementazione del metodo.

Quindi il lavoro puramente legato alla CPU sarebbe simile al seguente:

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

Che chiameresti usando Task.Run:

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

I metodi che sono una combinazione di CPU -bound e I / O-bound dovrebbero avere una Asyncfirma con documentazione che ne indichi la natura legata alla CPU:

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

Che chiameresti anche usando Task.Run(poiché è parzialmente associato alla CPU):

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

4
Grazie per la tua risposta veloce! Conosco il link che hai pubblicato e ho visto i video a cui fai riferimento nel tuo blog. In realtà è per questo che ho pubblicato questa domanda: nel video si dice (come nella tua risposta) non dovresti usare Task.Run nel codice core. Ma il mio problema è che devo racchiudere tale metodo ogni volta che lo uso per non rallentare le risposte (si noti che tutto il mio codice è asincrono e non si blocca, ma senza Thread.Run è solo ritardato). Sono anche confuso se è un approccio migliore per avvolgere semplicemente i metodi associati alla CPU (molte chiamate Task.Run) o avvolgere completamente tutto in un Task.Run?
Lukas K,

12
Tutti i metodi della tua libreria dovrebbero essere in uso ConfigureAwait(false). Se lo fai prima, allora potresti scoprire che Task.Runè completamente inutile. Se si fa ancora bisogno Task.Run, allora non fa molta differenza per la fase di esecuzione in questo caso se si chiama una volta o più volte, quindi basta fare ciò che è più naturale per il codice.
Stephen Cleary,

2
Non capisco come la prima tecnica lo aiuterà. Anche se usi ConfigureAwait(false)il tuo metodo cpu-bound, è comunque il thread dell'interfaccia utente che eseguirà il metodo cpu-bound e solo tutto ciò che può essere fatto su un thread TP. O ho frainteso qualcosa?
Dario il

4
@ user4205580: No, Task.Runcomprende le firme asincrone, quindi non verrà completato fino al DoWorkAsynccompletamento. Il extra async/ awaitnon è necessario. Spiego di più del "perché" nella mia serie di blog Task.Runsull'etichetta .
Stephen Cleary,

3
@ user4205580: No. La stragrande maggioranza dei metodi asincroni "core" non lo utilizza internamente. Il modo normale di implementare un metodo asincrono "core" è usare TaskCompletionSource<T>o una delle sue notazioni abbreviate come FromAsync. Ho un post sul blog che approfondisce il motivo per cui i metodi asincroni non richiedono discussioni .
Stephen Cleary,

12

Un problema con ContentLoader è che internamente funziona in sequenza. Un modello migliore è quello di parallelizzare il lavoro e poi sincronizzare alla fine, così otteniamo

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

Ovviamente, ciò non funziona se una qualsiasi delle attività richiede dati da altre attività precedenti, ma dovrebbe offrire un throughput complessivo migliore per la maggior parte degli scenari.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.