Quando usar corretamente Task.Run e quando apenas async-await

Eu gostaria de perguntar a você sobre sua opinião sobre a arquitetura correta quando usar o Task.Run . Eu estou experimentando lagging UI no nosso aplicativo WPF .NET 4.5 (com o framework Caliburn Micro).

Basicamente eu estou fazendo (trechos de código muito simplificados):

 public class PageViewModel : IHandle { ... 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(); } } 

Dos artigos / videos que eu li / vi, eu sei que await async não é necessariamente executado em um thread de fundo e para começar a trabalhar em segundo plano você precisa envolvê-lo com a espera Task.Run(async () => ... ) . Usar o async não bloqueia a interface do usuário, mas ainda está sendo executado no thread da interface do usuário, por isso está ficando lento.

Onde é o melhor lugar para colocar o Task.Run?

Eu deveria apenas

  1. Envolva a chamada externa porque isso é menos trabalho de threading para .NET

  2. , ou devo envolver apenas methods ligados à CPU executando internamente com Task.Run como isso torna reutilizável para outros lugares? Não tenho certeza se começar a trabalhar em tópicos de fundo no fundo é uma boa ideia.

Ad (1), a primeira solução seria assim:

 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), a segunda solução seria assim:

 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 } 

   

    Observe as diretrizes para executar o trabalho em um thread da interface do usuário , coletadas no meu blog:

    • Não bloqueie o thread da interface do usuário por mais de 50 ms por vez.
    • Você pode agendar ~ 100 continuações no thread da interface do usuário por segundo; 1000 é demais.

    Existem duas técnicas que você deve usar:

    1) Use ConfigureAwait(false) quando puder.

    Por exemplo, await MyAsync().ConfigureAwait(false); em vez de await MyAsync(); .

    ConfigureAwait(false) informa a await que você não precisa continuar no contexto atual (nesse caso, “no contexto atual” significa “no thread da interface do usuário”). No entanto, para o restante desse método async (após o ConfigureAwait ), você não pode fazer nada que supõe que esteja no contexto atual (por exemplo, atualizar elementos da interface do usuário).

    Para obter mais informações, consulte o artigo do MSDN sobre Práticas recomendadas de programação assíncrona .

    2) Use Task.Run para chamar methods de CPU.

    Você deve usar Task.Run , mas não dentro de qualquer código que você deseja reutilizar (por exemplo, código da biblioteca). Então você usa Task.Run para chamar o método, não como parte da implementação do método.

    Então, o trabalho puramente ligado à CPU ficaria assim:

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

    Qual você chamaria usando Task.Run :

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

    Métodos que são uma mistura de CPU-bound e I / O-bound devem ter uma assinatura Async com documentação apontando sua natureza de CPU:

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

    Que você também chamaria usando Task.Run (já que é parcialmente ligado à CPU):

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

    Um problema com o seu ContentLoader é que internamente ele opera sequencialmente. Um padrão melhor é paralelizar o trabalho e depois sincronizar no final.

     public class PageViewModel : IHandle { ... 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(); tasks.Add(DoCpuBoundWorkAsync()); tasks.Add(DoIoBoundWorkAsync()); tasks.Add(DoCpuBoundWorkAsync()); tasks.Add(DoSomeOtherWorkAsync()); await Task.WhenAll(tasks).ConfigureAwait(false); } } 

    Obviamente, isso não funciona se alguma das tarefas exigir dados de outras tarefas anteriores, mas deve fornecer uma taxa de transferência geral melhor para a maioria dos cenários.