Você precisa colocar o Task.Run em um método para torná-lo assíncrono?

304

Estou tentando entender async aguardam da forma mais simples. Eu quero criar um método muito simples que adicione dois números pelo bem deste exemplo, concedido, não há tempo de processamento, é apenas uma questão de formular um exemplo aqui.

Exemplo 1

private async Task DoWork1Async()
{
    int result = 1 + 2;
}

Exemplo 2

private async Task DoWork2Async()
{
    Task.Run( () =>
    {
        int result = 1 + 2;
    });
}

Se eu aguardar DoWork1Async(), o código será executado de forma síncrona ou assíncrona?

Preciso envolver o código de sincronização Task.Runpara tornar o método aguardável E assíncrono para não bloquear o thread da interface do usuário?

Estou tentando descobrir se meu método é um Taskou retorna Task<T>eu preciso envolver o código Task.Runpara torná-lo assíncrono.

Pergunta estúpida, tenho certeza, mas vejo exemplos na rede em que as pessoas aguardam código que não possui nada assíncrono e não está envolvido em um Task.Runou StartNew.

Neal
fonte
30
O seu primeiro snippet não está lhe dando nenhum aviso?
svick

Respostas:

587

Primeiro, vamos esclarecer algumas terminologias: "assíncrono" ( async) significa que ele pode retornar o controle ao thread de chamada antes de iniciar. Em um asyncmétodo, esses pontos de "rendimento" são awaitexpressões.

Isso é muito diferente do termo "assíncrono", pois (mis) usado pela documentação do MSDN por anos para significar "é executado em um thread de segundo plano".

Confundir ainda mais a questão, asyncé muito diferente de "aguardável"; existem alguns asyncmétodos cujos tipos de retorno não são aguardáveis ​​e muitos métodos que retornam tipos que não são aguardáveis async.

O suficiente sobre o que eles não são ; aqui estão o que são :

  • A asyncpalavra-chave permite um método assíncrono (ou seja, permite awaitexpressões). asyncmétodos podem retornar Task, Task<T>ou (se for necessário) void.
  • Qualquer tipo que segue um determinado padrão pode ser aguardado. Os tipos aguardáveis ​​mais comuns são Taske Task<T>.

Portanto, se reformularmos sua pergunta para "como posso executar uma operação em um encadeamento em segundo plano de uma maneira que seja aguardável", a resposta é usar Task.Run:

private Task<int> DoWorkAsync() // No async because the method does not need await
{
  return Task.Run(() =>
  {
    return 1 + 2;
  });
}

(Mas esse padrão é uma abordagem ruim; veja abaixo).

Mas se sua pergunta é "como criar um asyncmétodo que pode retornar ao chamador em vez de bloquear", a resposta é declarar o método asynce usá-lo awaitpara seus pontos de "retorno":

private async Task<int> GetWebPageHtmlSizeAsync()
{
  var client = new HttpClient();
  var html = await client.GetAsync("http://www.example.com/");
  return html.Length;
}

Portanto, o padrão básico das coisas é fazer com que o asynccódigo dependa de "aguardáveis" em suas awaitexpressões. Esses "aguardáveis" podem ser outros asyncmétodos ou apenas métodos regulares que retornam aguardáveis. Métodos regulares retornando Task/ Task<T> pode usar Task.Runpara executar código em um segmento de segundo plano, ou (mais comumente) eles podem usar TaskCompletionSource<T>ou um de seus atalhos ( TaskFactory.FromAsync, Task.FromResult, etc). Eu não recomendo envolvendo um método inteiro em Task.Run; Os métodos síncronos devem ter assinaturas síncronas e deve ser deixado ao consumidor se ele deve ser agrupado em Task.Run:

private int DoWork()
{
  return 1 + 2;
}

private void MoreSynchronousProcessing()
{
  // Execute it directly (synchronously), since we are also a synchronous method.
  var result = DoWork();
  ...
}

private async Task DoVariousThingsFromTheUIThreadAsync()
{
  // I have a bunch of async work to do, and I am executed on the UI thread.
  var result = await Task.Run(() => DoWork());
  ...
}

Eu tenho um async/ awaitintrodução no meu blog; no final, existem alguns bons recursos de acompanhamento. Os documentos do MSDN também asyncsão extraordinariamente bons.

Stephen Cleary
fonte
8
@sgnsajgon: Sim. asyncmétodos devem retornar Task, Task<T>ou void. Taske Task<T>são aguardáveis; voidnão é.
Stephen Cleary
3
Na verdade, a async voidassinatura do método irá compilar, é apenas uma idéia muito terrível como você solta o ponteiro para a tarefa assíncrona
IEatBagels
4
@TopinFrassi: Sim, eles serão compilados, mas voidnão são aguardáveis.
Stephen Cleary
4
@ ohadinho: Não, o que estou falando no post do blog é quando todo o método é apenas uma chamada para Task.Run(como DoWorkAsyncnesta resposta). Usar Task.Runpara chamar um método de um contexto de interface do usuário é apropriado (como DoVariousThingsFromTheUIThreadAsync).
21416 Stephen Hawy
2
Sim, exatamente. É válido usar Task.Runpara invocar um método, mas se houver um Task.Runcódigo em torno de todo (ou quase todo) do método, esse é um anti-padrão - apenas mantenha esse método sincronizado e Task.Runsuba de nível.
Stephen Cleary
22

Uma das coisas mais importantes a serem lembradas ao decorar um método com assíncrono é que pelo menos há um operador aguardado dentro do método. No seu exemplo, eu o traduziria como mostrado abaixo usando TaskCompletionSource .

private Task<int> DoWorkAsync()
{
    //create a task completion source
    //the type of the result value must be the same
    //as the type in the returning Task
    TaskCompletionSource<int> tcs = new TaskCompletionSource<int>();
    Task.Run(() =>
    {
        int result = 1 + 2;
        //set the result to TaskCompletionSource
        tcs.SetResult(result);
    });
    //return the Task
    return tcs.Task;
}

private async void DoWork()
{
    int result = await DoWorkAsync();
}
Ronald Ramos
fonte
26
Por que você usa TaskCompletionSource, em vez de apenas retornar a tarefa retornada pelo método Task.Run () (e alterar seu corpo para retornar o resultado)?
ironic,
4
Apenas uma nota lateral. Um método que possui uma assinatura "vazio assíncrono" geralmente é uma prática ruim e é considerado código incorreto, pois pode levar ao impasse da interface do usuário com bastante facilidade. A principal exceção são os manipuladores de eventos assíncronos.
Jazzeroki
12

Quando você usa Task.Run para executar um método, o Task obtém um thread do threadpool para executar esse método. Portanto, da perspectiva do thread da interface do usuário, é "assíncrono", pois não bloqueia o thread da interface do usuário. Isso é bom para aplicativos de desktop, pois geralmente você não precisa de muitos threads para cuidar das interações do usuário.

No entanto, para aplicativos da Web, cada solicitação é atendida por um encadeamento do conjunto de encadeamentos e, portanto, o número de solicitações ativas pode ser aumentado ao salvar esses encadeamentos. O uso frequente de threads do conjunto de threads para simular operações assíncronas não é escalável para aplicativos da Web.

O True Async não envolve necessariamente o uso de um encadeamento para operações de E / S, como acesso a arquivos / bancos de dados, etc. Você pode ler isso para entender por que a operação de E / S não precisa de encadeamentos. http://blog.stephencleary.com/2013/11/there-is-no-thread.html

No seu exemplo simples, é um cálculo vinculado à CPU puro, portanto, usar o Task.Run é bom.

zheng yu
fonte
Portanto, se eu tiver que consumir uma API externa síncrona dentro de um controlador de API da Web, NÃO devo encerrar a chamada síncrona em Task.Run ()? Como você disse, isso manterá o thread de solicitação inicial desbloqueado, mas ele está usando outro thread de pool para chamar a API externa. Na verdade, acho que ainda é uma boa ideia, pois dessa maneira, em teoria, é possível usar dois threads de pool para processar muitos pedidos. Por exemplo, um thread pode processar muitos pedidos recebidos e outro pode chamar a API externa para todos esses pedidos?
stt106
Estou de acordo, não estou dizendo que você não deve encerrar absolutamente todas as chamadas síncronas no Task.Run (). Estou simplesmente apontando um problema em potencial.
zheng yu 22/03
1
@ stt106 I should NOT wrap the synchronous call in Task.Run()está correto. Se fizer isso, você apenas trocaria de tópicos. ou seja, você está desbloqueando o thread de solicitação inicial, mas está usando outro thread do pool de threads que poderia ter sido usado para processar outra solicitação. O único resultado é uma sobrecarga de troca de contexto quando a chamada é completada por absolutamente ganho de zero
Saeb Amini