Como você cria um método assíncrono em c #?

196

Cada postagem de blog que eu li mostra como consumir um método assíncrono em C #, mas, por algum motivo estranho, nunca explique como criar seus próprios métodos assíncronos para consumir. Então, eu tenho esse código agora que consome meu método:

private async void button1_Click(object sender, EventArgs e)
{
    var now = await CountToAsync(1000);
    label1.Text = now.ToString();
}

E eu escrevi este método que é CountToAsync:

private Task<DateTime> CountToAsync(int num = 1000)
{
    return Task.Factory.StartNew(() =>
    {
        for (int i = 0; i < num; i++)
        {
            Console.WriteLine("#{0}", i);
        }
    }).ContinueWith(x => DateTime.Now);
}

Essa é Task.Factorya melhor maneira de escrever um método assíncrono ou devo escrever de outra maneira?

Khalid Abuhakmeh
fonte
22
Estou fazendo uma pergunta geral sobre como estruturar um método. Eu só quero saber por onde começar a transformar meus métodos já síncronos em métodos assíncronos.
Khalid Abuhakmeh
2
OK, então o que é que um método síncrono típico fazer , e por que você quer para torná-lo assíncrona ?
precisa
Digamos que eu precise processar em lote um monte de arquivos e retornar um objeto de resultado.
Khalid Abuhakmeh
1
OK, então: (1) qual é a operação de alta latência: obter os arquivos - porque a rede pode estar lenta, ou o que for - ou fazer o processamento - porque consome muita CPU, por exemplo. E (2) você ainda não disse por que deseja que seja assíncrono em primeiro lugar. Existe um thread da interface do usuário que você não deseja bloquear ou o quê?
precisa
21
@EricLippert O exemplo dado pelo op é muito básico, ele realmente não precisa ser tão complicado.
David B.

Respostas:

226

Eu não recomendo, a StartNewmenos que você precise desse nível de complexidade.

Se o seu método assíncrono depende de outros métodos assíncronos, a abordagem mais fácil é usar a asyncpalavra-chave:

private static async Task<DateTime> CountToAsync(int num = 10)
{
  for (int i = 0; i < num; i++)
  {
    await Task.Delay(TimeSpan.FromSeconds(1));
  }

  return DateTime.Now;
}

Se o seu método assíncrono estiver executando o trabalho da CPU, você deve usar Task.Run:

private static async Task<DateTime> CountToAsync(int num = 10)
{
  await Task.Run(() => ...);
  return DateTime.Now;
}

Você pode achar minha async/ awaitintrodução útil.

Stephen Cleary
fonte
10
@ Stephen: "Se o seu método assíncrono depende de outros métodos assíncronos" - ok, mas e se não for esse o caso. E se estivesse tentando quebrar algum código que chama BeginInvoke com algum código de retorno de chamada?
Ricibob
1
@ Ricibob: Você deve usar TaskFactory.FromAsyncpara embrulhar BeginInvoke. Não sabe ao certo o que você quer dizer com "código de retorno de chamada"; fique à vontade para postar sua própria pergunta com código.
Stephen Cleary
@ Stephen: Obrigado - sim TaskFactory.FromAsync é o que eu estava procurando.
Ricibob
1
Stephen, no loop for, a próxima iteração é chamada imediatamente ou após os Task.Delayretornos?
Jp2code
3
@ jp2code: awaité uma "espera assíncrona", portanto, não prossegue para a próxima iteração até que a tarefa retornada seja Task.Delayconcluída.
Stephen Cleary
11

Se você não quis usar async / waitit dentro do seu método, mas ainda o "decorou" para poder usar a palavra-chave wait de fora, TaskCompletionSource.cs :

public static Task<T> RunAsync<T>(Func<T> function)
{ 
    if (function == null) throw new ArgumentNullException(“function”); 
    var tcs = new TaskCompletionSource<T>(); 
    ThreadPool.QueueUserWorkItem(_ =>          
    { 
        try 
        {  
           T result = function(); 
           tcs.SetResult(result);  
        } 
        catch(Exception exc) { tcs.SetException(exc); } 
   }); 
   return tcs.Task; 
}

Daqui e aqui

Para suportar esse paradigma com as Tarefas, precisamos de uma maneira de manter a fachada da Tarefa e a capacidade de nos referir a uma operação assíncrona arbitrária como uma Tarefa, mas controlar o tempo de vida dessa Tarefa de acordo com as regras da infraestrutura subjacente que fornece o assincronia e fazê-lo de uma maneira que não custe significativamente. Esse é o objetivo do TaskCompletionSource.

Eu vi também é usado na fonte .NET, por exemplo. WebClient.cs :

    [HostProtection(ExternalThreading = true)]
    [ComVisible(false)]
    public Task<string> UploadStringTaskAsync(Uri address, string method, string data)
    {
        // Create the task to be returned
        var tcs = new TaskCompletionSource<string>(address);

        // Setup the callback event handler
        UploadStringCompletedEventHandler handler = null;
        handler = (sender, e) => HandleCompletion(tcs, e, (args) => args.Result, handler, (webClient, completion) => webClient.UploadStringCompleted -= completion);
        this.UploadStringCompleted += handler;

        // Start the async operation.
        try { this.UploadStringAsync(address, method, data, tcs); }
        catch
        {
            this.UploadStringCompleted -= handler;
            throw;
        }

        // Return the task that represents the async operation
        return tcs.Task;
    }

Finalmente, achei útil também o seguinte:

Eu recebo essa pergunta o tempo todo. A implicação é que deve haver algum encadeamento em algum lugar que esteja bloqueando a chamada de E / S para o recurso externo. Então, o código assíncrono libera o encadeamento de solicitação, mas apenas às custas de outro encadeamento em outro local do sistema, certo? Não, não mesmo. Para entender por que as solicitações assíncronas são dimensionadas, traçarei um exemplo (simplificado) de uma chamada de E / S assíncrona. Digamos que uma solicitação precise gravar em um arquivo. O encadeamento de solicitação chama o método de gravação assíncrono. O WriteAsync é implementado pela Base Class Library (BCL) e usa portas de conclusão para sua E / S assíncrona. Portanto, a chamada WriteAsync é transmitida ao sistema operacional como uma gravação assíncrona de arquivo. O sistema operacional se comunica com a pilha do driver, passando os dados para gravar em um pacote de solicitação de E / S (IRP). É onde as coisas começam a ficar interessantes: Se um driver de dispositivo não puder lidar com um IRP imediatamente, ele deverá ser tratado de forma assíncrona. Portanto, o driver diz ao disco para começar a gravar e retorna uma resposta "pendente" para o sistema operacional. O SO passa essa resposta "pendente" para a BCL, e a BCL retorna uma tarefa incompleta ao código de tratamento de solicitações. O código de tratamento de solicitações aguarda a tarefa, que retorna uma tarefa incompleta desse método e assim por diante. Por fim, o código de tratamento de solicitações acaba retornando uma tarefa incompleta ao ASP.NET e o encadeamento de solicitação é liberado para retornar ao pool de encadeamentos. O código de tratamento de solicitações aguarda a tarefa, que retorna uma tarefa incompleta desse método e assim por diante. Por fim, o código de tratamento de solicitações acaba retornando uma tarefa incompleta ao ASP.NET e o encadeamento de solicitação é liberado para retornar ao pool de encadeamentos. O código de tratamento de solicitações aguarda a tarefa, que retorna uma tarefa incompleta desse método e assim por diante. Por fim, o código de tratamento de solicitações acaba retornando uma tarefa incompleta ao ASP.NET e o encadeamento de solicitação é liberado para retornar ao pool de encadeamentos.

Introdução ao Async / Await no ASP.NET

Se o objetivo é melhorar a escalabilidade (em vez de capacidade de resposta), tudo depende da existência de uma E / S externa que forneça a oportunidade de fazer isso.

Alberto
fonte
1
Se você vir o comentário acima da implementação do Task.Run, por exemplo, enfileira o trabalho especificado para executar no ThreadPool e retorna um identificador de tarefa para esse trabalho . Você realmente faz o mesmo. Tenho certeza de que o Task.Run será melhor, pois ele gerencia internamente o CancelationToken e faz algumas otimizações com as opções de agendamento e execução.
unsafePtr
então o que você fez com ThreadPool.QueueUserWorkItem poderia ser feito com Task.Run, certo?
Razvan
-1

Uma maneira muito simples de tornar um método assíncrono é usar o método Task.Yield (). Como o MSDN declara:

Você pode usar Aguardar Task.Yield (); em um método assíncrono para forçar a conclusão assíncrona do método.

Insira-o no início do seu método e ele retornará imediatamente ao chamador e concluirá o restante do método em outro thread.

private async Task<DateTime> CountToAsync(int num = 1000)
{
    await Task.Yield();
    for (int i = 0; i < num; i++)
    {
        Console.WriteLine("#{0}", i);
    }
    return DateTime.Now;
}
SalgoMato
fonte
Em um aplicativo WinForms, o restante do método será concluído no mesmo thread (o thread da interface do usuário). A Task.Yield()é realmente útil, para os casos que você deseja garantir que o retornado Tasknão será imediatamente completado após a criação.
Theodor Zoulias