Quando usar corretamente o Task.Run e quando apenas async-waitit

317

Gostaria de perguntar sua opinião sobre a arquitetura correta quando usar Task.Run. Estou com a interface do usuário atrasada em nosso aplicativo WPF .NET 4.5 (com estrutura Caliburn Micro).

Basicamente, estou fazendo (trechos de código muito 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();
    }
}

Pelos artigos / vídeos que li / vi, sei que await asyncnão é necessariamente executado em um thread em segundo plano e para iniciar o trabalho em segundo plano, você precisa envolvê-lo em espera Task.Run(async () => ... ). O uso async awaitnão bloqueia a interface do usuário, mas continua sendo executado no encadeamento da interface do usuário, tornando-o lento.

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

Devo apenas

  1. Encerre a chamada externa porque isso é menos trabalhoso para o .NET

  2. , ou devo agrupar apenas métodos vinculados à CPU em execução internamente, Task.Runpois isso o reutiliza para outros lugares? Não tenho certeza aqui se iniciar um trabalho em threads de fundo no fundo é uma boa idéia.

Anúncio (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.

Anúncio (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
}
Lukas K
fonte
BTW, a linha em (1) await Task.Run(async () => await this.contentLoader.LoadContentAsync());deve ser simplesmente await Task.Run( () => this.contentLoader.LoadContentAsync() );. AFAIK você não ganha nada adicionando um segundo awaite asyncdentro Task.Run. E como você não está transmitindo parâmetros, isso simplifica um pouco mais a await Task.Run( this.contentLoader.LoadContentAsync );.
Home
na verdade, há uma pequena diferença se você tiver um segundo a aguardar lá dentro. Veja este artigo . Achei muito útil, apenas com esse ponto em particular não concordo e prefiro retornar diretamente a Tarefa em vez de aguardar. (como você sugere em seu comentário)
Lukas K

Respostas:

365

Observe as diretrizes para executar o trabalho em um thread da interface do usuário , coletado 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, em await MyAsync().ConfigureAwait(false);vez de await MyAsync();.

ConfigureAwait(false)informa awaitque você não precisa continuar no contexto atual (nesse caso, "no contexto atual" significa "no encadeamento da interface do usuário"). No entanto, para o restante desse asyncmétodo (após o ConfigureAwait), você não pode fazer nada que pressuponha que você esteja no contexto atual (por exemplo, atualize os elementos da interface do usuário).

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

2) Use Task.Runpara chamar métodos ligados à CPU.

Você deve usar Task.Run, mas não dentro de qualquer código que deseja que seja reutilizável (ou seja, código de biblioteca). Então você usa Task.Runpara chamar o método, não como parte da implementação do método.

Portanto, o trabalho puramente vinculado à CPU ficaria assim:

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

Você chamaria usando Task.Run:

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

Os métodos que são uma mistura de limite de CPU e de E / S devem ter uma Asyncassinatura com documentação apontando sua natureza de limite de CPU:

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

Você também chamaria de usar Task.Run(já que é parcialmente vinculado à CPU):

await Task.Run(() => DoWorkAsync());
Stephen Cleary
fonte
4
Obrigado pelo seu rápido! Conheço o link que você postou e vi os vídeos que são referenciados no seu blog. Na verdade, foi por isso que eu postei essa pergunta - no vídeo é dito (o mesmo que na sua resposta), você não deve usar o Task.Run no código principal. Mas o meu problema é que preciso envolver esse método toda vez que o usar para não diminuir a velocidade das respostas (observe que todo o meu código é assíncrono e não está bloqueando, mas sem o Thread.Run, é apenas um atraso). Também estou confuso se é a melhor abordagem para quebrar apenas os métodos vinculados à CPU (muitas chamadas de Task.Run) ou envolver completamente tudo em um Task.Run?
Lukas K
12
Todos os métodos da sua biblioteca devem estar usando ConfigureAwait(false). Se você fizer isso primeiro, poderá achar que isso Task.Runé completamente desnecessário. Se você não ainda precisa Task.Run, então não faz muita diferença para o tempo de execução, neste caso, se você chamá-lo uma vez ou muitas vezes, por isso basta fazer o que é mais natural para o seu código.
Stephen Cleary
2
Não entendo como a primeira técnica o ajudará. Mesmo se você usar ConfigureAwait(false)no método vinculado à CPU, ainda será o thread da interface do usuário que fará o método vinculado à CPU, e somente tudo o que estiver a seguir poderá ser feito em um thread TP. Ou entendi algo errado?
Darius
4
@ user4205580: Não, Task.Runentende assinaturas assíncronas, portanto não será concluída até que DoWorkAsyncseja concluída. O extra async/ awaité desnecessário. Eu explico mais sobre o "porquê" na minha série de blogs sobre Task.Runetiqueta .
Stephen Cleary
3
@ user4205580: Não. A grande maioria dos métodos assíncronos "básicos" não o utilizam internamente. A maneira normal de implementar um método assíncrono "básico" é usar TaskCompletionSource<T>ou uma de suas notações abreviadas, como FromAsync. Eu tenho uma postagem no blog que entra em mais detalhes por que os métodos assíncronos não requerem threads .
Stephen Cleary
12

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

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, isso não funciona se alguma das tarefas exigir dados de outras tarefas anteriores, mas deve oferecer melhor rendimento geral para a maioria dos cenários.

Paul Hatcher
fonte