Este artigo é bastante antigo, mas não perdeu sua relevância. Quando se trata de assíncrono / espera, geralmente aparece um link para ele. Não consegui encontrar uma tradução para o russo, decidi ajudar alguém que não é fluente.
A programação assíncrona tem sido o reino dos desenvolvedores mais experientes com desejo de masoquismo - aqueles que tinham tempo livre suficiente, inclinação e capacidade psíquica para pensar em retornos de retorno de retorno de chamada em um fluxo não linear de execução. Com o advento do Microsoft .NET Framework 4.5, o C # e o Visual Basic nos trouxeram assincronia, para que meros mortais agora possam escrever métodos assíncronos quase tão facilmente quanto os síncronos. Retornos de chamada não são mais necessários. Não há mais código explícito de empacotamento de um contexto de sincronização para outro. Não há mais preocupações sobre como os resultados ou exceções da execução se movem. Não há necessidade de truques que distorcem os meios das linguagens de programação para a conveniência de desenvolver código assíncrono. Em suma, não há mais problemas e dor de cabeça.
Obviamente, embora agora seja fácil começar a escrever métodos assíncronos (consulte os artigos de Eric Lippert e Mads Torgersen nesta MSDN Magazine [outubro de 2011] ), é necessário entendimento para fazer isso corretamente. o que acontece debaixo do capô. Sempre que uma linguagem ou biblioteca aumenta o nível de abstração que um desenvolvedor pode usar, isso é inevitavelmente acompanhado por custos ocultos que reduzem a produtividade. Em muitos casos, esses custos são insignificantes e, portanto, podem ser negligenciados na maioria dos casos pela maioria dos programadores. No entanto, desenvolvedores avançados devem entender completamente quais custos estão presentes para tomar as medidas necessárias e resolver possíveis problemas, se eles se manifestarem. Isso é necessário ao usar ferramentas de programação assíncronas em C # e Visual Basic.
Neste artigo, descreverei as entradas e saídas de métodos assíncronos, descreverei como os métodos assíncronos são implementados e discutirei alguns dos custos menores. Observe que essa não é uma recomendação para distorcer o código legível em algo difícil de manter, em nome da microoptimização e desempenho. Esse é apenas o conhecimento que ajudará a diagnosticar problemas que você pode encontrar e um conjunto de ferramentas para superá-los. Além disso, este artigo é baseado na visualização do .NET Framework versão 4.5 e provavelmente os detalhes específicos da implementação podem mudar na versão final.
Obtenha um modelo de pensamento confortável
Por décadas, os programadores têm usado as linguagens de programação de alto nível C #, Visual Basic, F # e C ++ para desenvolver aplicativos produtivos. Essa experiência permitiu aos programadores avaliar os custos de várias operações e obter conhecimento sobre as melhores técnicas de desenvolvimento. Por exemplo, na maioria dos casos, chamar um método síncrono é relativamente econômico, especialmente se o compilador puder incorporar o conteúdo do método chamado diretamente no ponto de chamada. Portanto, os desenvolvedores estão acostumados a dividir o código em métodos pequenos e fáceis de manter, sem precisar se preocupar com as consequências negativas do aumento do número de chamadas. O modelo de pensamento desses programadores é projetado para lidar com chamadas de método.
Com o advento dos métodos assíncronos, é necessário um novo modelo de pensamento. C # e Visual Basic com seus compiladores são capazes de criar a ilusão de que o método assíncrono funciona como seu equivalente síncrono, embora tudo esteja completamente errado por dentro. O compilador gera uma enorme quantidade de código para o programador, muito semelhante ao modelo padrão que os desenvolvedores escreveram para dar suporte à assincronia durante o tempo em que era necessário fazê-lo manualmente. Além disso, o código gerado pelo compilador contém chamadas para as funções de biblioteca do .NET Framework, reduzindo ainda mais a quantidade de trabalho que um programador precisa realizar. Para ter o modelo certo de pensamento e usá-lo para tomar decisões informadas, é importante entender o que o compilador gera para você.
Mais métodos, menos chamadas
Ao trabalhar com código síncrono, a execução de métodos com conteúdo vazio é praticamente inútil. Para métodos assíncronos, esse não é o caso. Considere este método assíncrono, consistindo em uma instrução (e que, devido à falta de instruções de espera, será executada de forma síncrona):
public static async Task SimpleBodyAsync() { Console.WriteLine("Hello, Async World!"); }
Um descompilador de linguagem intermediária (IL) revelará o verdadeiro conteúdo dessa função após a compilação, produzindo algo semelhante à Figura 1. O que era um liner simples transformado em dois métodos, um dos quais pertence à classe auxiliar da máquina de estado. O primeiro é um método stub que possui uma assinatura semelhante à escrita pelo programador (este método tem o mesmo nome, o mesmo escopo, usa os mesmos parâmetros e retorna o mesmo tipo), mas não contém código escrito pelo programador. Ele contém apenas um padrão para a configuração inicial. O código de configuração inicial inicializa a máquina de estado necessária para representar o método assíncrono e a inicia usando uma chamada para o método do utilitário MoveNext. O tipo de objeto da máquina de estado contém uma variável com o estado de execução do método assíncrono, permitindo que você a salve ao alternar entre pontos de espera assíncronos. Ele também contém código gravado por um programador, modificado para garantir a transferência de resultados de execução e exceções para o objeto Task retornado; mantendo a posição atual no método para que a execução possa continuar a partir dessa posição depois de retomar etc.
Figura 1 Modelo de método assíncrono
[DebuggerStepThrough] public static Task SimpleBodyAsync() { <SimpleBodyAsync>d__0 d__ = new <SimpleBodyAsync>d__0(); d__.<>t__builder = AsyncTaskMethodBuilder.Create(); d__.MoveNext(); return d__.<>t__builder.Task; } [CompilerGenerated] [StructLayout(LayoutKind.Sequential)] private struct <SimpleBodyAsync>d__0 : <>t__IStateMachine { private int <>1__state; public AsyncTaskMethodBuilder <>t__builder; public Action <>t__MoveNextDelegate; public void MoveNext() { try { if (this.<>1__state == -1) return; Console.WriteLine("Hello, Async World!"); } catch (Exception e) { this.<>1__state = -1; this.<>t__builder.SetException(e); return; } this.<>1__state = -1; this.<>t__builder.SetResult(); } ... }
Quando você se perguntar quanto custa as chamadas para métodos assíncronos, lembre-se desse padrão. O bloco try / catch no método MoveNext é necessário para impedir uma possível tentativa de incorporar esse método pelo JIT pelo compilador, portanto, pelo menos, obtemos o custo de chamar o método, enquanto, ao usar o método síncrono, provavelmente essa chamada não será (desde conteúdo minimalista). Receberemos várias chamadas para os procedimentos do Framework (por exemplo, SetResult). Bem como várias operações de gravação nos campos do objeto de máquina de estado. Obviamente, precisamos comparar todos esses custos com os do Console.WriteLine, que provavelmente prevalecerá (eles incluem os custos de bloqueio, E / S, etc.) Preste atenção às otimizações que o ambiente faz para você. Por exemplo, um objeto de uma máquina de estado é implementado como uma estrutura (struct). Essa estrutura será encaixotada em um heap gerenciado apenas se o método precisar pausar a execução, aguardando a conclusão da operação, e isso nunca acontecerá neste método simples. Portanto, o padrão desse método assíncrono não exigirá alocação de memória do heap. O compilador e o tempo de execução tentarão minimizar o número de operações de alocação de memória.
Quando não usar o Async
O .NET Framework tenta gerar implementações eficientes para métodos assíncronos usando vários métodos de otimização. No entanto, os desenvolvedores, com base em sua experiência, geralmente aplicam seus métodos de otimização, que podem ser arriscados e impraticáveis para automação pelo compilador e pelo tempo de execução, enquanto tentam usar abordagens universais. Se você não esquecer disso, a rejeição do uso de métodos assíncronos é benéfica em vários casos específicos, em particular, isso se aplica a métodos em bibliotecas que podem ser usadas com configurações mais refinadas. Geralmente isso acontece quando se sabe ao certo que o método pode ser executado de forma síncrona, pois os dados dos quais depende já estão prontos.
Ao criar métodos assíncronos, os desenvolvedores do .NET Framework gastaram muito tempo otimizando o número de operações de gerenciamento de memória. Isso é necessário porque o gerenciamento de memória incorre no maior custo no desempenho de uma infraestrutura assíncrona. A operação de alocar memória para um objeto geralmente é relativamente barata. Alocar memória para objetos é semelhante a encher o carrinho com produtos no supermercado - você não gasta nada quando os coloca no carrinho. Os gastos ocorrem quando você paga no caixa, retira sua carteira e dá dinheiro decente. E se a alocação de memória for fácil, a coleta de lixo subsequente poderá afetar seriamente o desempenho do aplicativo. Quando você inicia a coleta de lixo, a varredura e a marcação de objetos atualmente localizados na memória, mas sem links, são executadas. Quanto mais objetos são colocados, mais tempo leva para marcá-los. Além disso, quanto maior o número de objetos de tamanho grande colocados, mais frequentemente é necessária a coleta de lixo. Esse aspecto do trabalho com memória tem um impacto global no sistema: quanto mais lixo é produzido por métodos assíncronos, mais lento o aplicativo é executado, mesmo que os microtestes não demonstrem custos significativos.
Para métodos assíncronos que suspendem sua execução (aguardando dados que ainda não estão prontos), o ambiente deve criar um objeto do tipo Tarefa, que será retornado do método, pois esse objeto serve como uma referência exclusiva para a chamada. No entanto, chamadas de método frequentemente assíncronas podem ser feitas sem suspensão. Em seguida, o tempo de execução pode retornar do cache o objeto Task concluído anteriormente, que é usado repetidamente sem a necessidade de criar novos objetos Task. É verdade que isso é permitido apenas sob certas condições, por exemplo, quando o método assíncrono retorna um objeto não universal (não genérico), Task, Task ou quando a tarefa universal é especificada por um tipo de referência TResult e null é retornado do método. Embora a lista dessas condições esteja se expandindo ao longo do tempo, ainda é melhor se você souber como a operação é implementada.
Considere uma implementação como MemoryStream. MemoryStream é herdado do Stream e redefine os novos métodos implementados no .NET 4.5: ReadAsync, WriteAsync e FlushAsync, a fim de fornecer otimização de código específica da memória. Como a operação de leitura é realizada a partir de um buffer localizado na memória, ou seja, na verdade é uma cópia da área de memória, o melhor desempenho será se o ReadAsync for executado no modo síncrono. Uma implementação disso em um método assíncrono pode ser assim:
public override async Task<int> ReadAsync(byte [] buffer, int offset, int count, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); return this.Read(buffer, offset, count); }
Simples o suficiente. E como o Read é uma chamada síncrona e o método não tem instruções de espera para controlar as expectativas, todas as chamadas para este ReadAsync serão realmente executadas de forma síncrona. Agora, vamos ver um caso padrão de uso de threads, por exemplo, uma operação de cópia:
byte [] buffer = new byte[0x1000]; int numRead; while((numRead = await source.ReadAsync(buffer, 0, buffer.Length)) > 0) { await source.WriteAsync(buffer, 0, numRead); }
Observe que no exemplo ReadAsync fornecido, o fluxo de origem é sempre chamado com o mesmo parâmetro de tamanho do buffer, o que significa que é muito provável que o valor de retorno (o número de bytes lidos) também seja repetido. Exceto em algumas circunstâncias raras, é improvável que a implementação do ReadAsync use o objeto de tarefa em cache como o valor de retorno, mas você pode fazê-lo.
Considere outra opção de implementação para esse método, mostrada na Figura 2. Usando as vantagens de seus aspectos inerentes nos scripts padrão para esse método, podemos otimizar a implementação excluindo operações de alocação de memória, que é improvável que seja esperado do tempo de execução. Podemos eliminar completamente a perda de memória retornando o mesmo objeto Task que foi usado na chamada ReadAsync anterior, se o mesmo número de bytes tiver sido lido. E para uma operação de baixo nível, que provavelmente será muito rápida e será chamada repetidamente, essa otimização terá um efeito significativo, especialmente no número de coletas de lixo.
Figura 2 Otimização da criação de tarefas
private Task<int> m_lastTask; public override Task<int> ReadAsync(byte [] buffer, int offset, int count, CancellationToken cancellationToken) { if (cancellationToken.IsCancellationRequested) { var tcs = new TaskCompletionSource<int>(); tcs.SetCanceled(); return tcs.Task; } try { int numRead = this.Read(buffer, offset, count); return m_lastTask != null && numRead == m_lastTask.Result ? m_lastTask : (m_lastTask = Task.FromResult(numRead)); } catch(Exception e) { var tcs = new TaskCompletionSource<int>(); tcs.SetException(e); return tcs.Task; } }
Um método de otimização semelhante, eliminando a criação desnecessária de objetos Task, pode ser usado se o armazenamento em cache for necessário. Considere um método desenvolvido para recuperar o conteúdo de uma página da Web e armazená-lo em cache para referência futura. Como método assíncrono, isso pode ser escrito da seguinte maneira (usando a nova biblioteca System.Net.Http.dll para .NET 4.5):
private static ConcurrentDictionary<string,string> s_urlToContents; public static async Task<string> GetContentsAsync(string url) { string contents; if (!s_urlToContents.TryGetValue(url, out contents)) { var response = await new HttpClient().GetAsync(url); contents = response.EnsureSuccessStatusCode().Content.ReadAsString(); s_urlToContents.TryAdd(url, contents); } return contents; }
Esta é uma implementação de testa. E para chamadas GetContentsAsync que não encontram dados no cache, a sobrecarga de criação de um novo objeto de Tarefa pode ser negligenciada em comparação com o custo de recebimento de dados pela rede. No entanto, no caso de obter dados do cache, esses custos se tornam significativos se você simplesmente agrupar e fornecer dados locais disponíveis.
Para eliminar esses custos (se necessário para obter alto desempenho), você pode reescrever o método conforme mostrado na Figura 3. Agora, temos dois métodos: um método público síncrono e um método privado assíncrono, aos quais o público delega. A coleção Dictionary agora armazena em cache os objetos Task criados, e não seu conteúdo, para que futuras tentativas de recuperar o conteúdo de uma página obtida anteriormente com êxito possam ser realizadas, basta acessar a coleção para retornar o objeto Task existente. Por dentro, você pode tirar vantagem do uso dos métodos ContinueWith do objeto Task, que nos permitem salvar o objeto executado na coleção - caso o carregamento da página tenha sido bem-sucedido. Obviamente, esse código é mais complexo e requer muito desenvolvimento e suporte, como de costume ao otimizar o desempenho: você não quer gastar tempo escrevendo-o até que o teste de desempenho mostre que essas complicações levam a sua melhoria, o que é impressionante e óbvio. Quais melhorias realmente dependerão do método de aplicação. Você pode fazer um conjunto de testes que simula casos de uso comuns e avaliar os resultados para determinar se o jogo vale a pena.
Figura 3 Tarefas de armazenamento em cache manualmente
private static ConcurrentDictionary<string,Task<string>> s_urlToContents; public static Task<string> GetContentsAsync(string url) { Task<string> contents; if (!s_urlToContents.TryGetValue(url, out contents)) { contents = GetContentsInternalAsync(url); contents.ContinueWith(delegate { s_urlToContents.TryAdd(url, contents); }, CancellationToken.None, TaskContinuationOptions.OnlyOnRanToCompletion | TaskContinuatOptions.ExecuteSynchronously, TaskScheduler.Default); } return contents; } private static async Task<string> GetContentsInternalAsync(string url) { var response = await new HttpClient().GetAsync(url); return response.EnsureSuccessStatusCode().Content.ReadAsString(); }
Outro método de otimização associado aos objetos Task é determinar se um objeto desse tipo é retornado pelo método assíncrono. O C # e o Visual Basic oferecem suporte a métodos assíncronos que retornam um valor nulo (nulo) e não criam objetos de tarefa. Métodos assíncronos nas bibliotecas sempre devem retornar Tarefa e Tarefa, pois ao projetar uma biblioteca, você não pode saber que eles não serão usados aguardando a conclusão. No entanto, ao desenvolver aplicativos, os métodos que retornam nulos podem encontrar seu lugar. O principal motivo da existência de tais métodos é fornecer ambientes orientados a eventos existentes, como ASP.NET e Windows Presentation Foundation (WPF). Usando async e wait, esses métodos facilitam a implementação de manipuladores de botão, eventos de carregamento de página etc. Se você pretende usar um método assíncrono com void, tenha cuidado ao lidar com exceções: exceções aparecerão em qualquer SynchronizationContext que estava ativo no momento em que o método foi chamado.
Não esqueça o contexto
Existem muitos contextos diferentes no .NET Framework: LogicalCallContext, SynchronizationContext, HostExecutionContext, SecurityContext, ExecutionContext e outros (sua quantidade gigantesca pode sugerir que os criadores do Framework foram motivados financeiramente para criar novos contextos, mas sei com certeza que isso não é assim). Alguns desses contextos afetam fortemente métodos assíncronos, não apenas em termos de funcionalidade, mas também em desempenho.
SynchronizationContext SynchronizationContext desempenha um papel importante nos métodos assíncronos. Um "contexto de sincronização" é apenas uma abstração para garantir que uma chamada de delegado com os detalhes de uma determinada biblioteca ou ambiente seja empacotada. Por exemplo, o WPF possui um DispatcherSynchronizationContext para representar um thread da interface do usuário do Dispatcher: o envio de um representante para esse contexto de sincronização faz com que esse representante seja colocado na fila para execução pelo Dispatcher em seu thread. O ASP.NET fornece um AspNetSynchronizationContext usado para garantir que as operações assíncronas envolvidas no processamento de uma solicitação ASP.NET sejam executadas seqüencialmente e estejam vinculadas ao estado correto HttpContext. Bem, etc. Em geral, existem cerca de 10 especializações do SynchronizationContext no .NET Framework, algumas abertas, outras internas.
Ao aguardar tarefas ou objetos de outros tipos pelos quais o .NET Framework pode implementar isso, os objetos que esperam por eles (por exemplo, TaskAwaiter) capturam o SynchronizationContext atual no momento em que a espera (espera) começa. Após a conclusão da espera, se o SynchronizationContext foi capturado, a continuação do método assíncrono é enviada para esse contexto de sincronização. Devido a isso, os programadores que escrevem métodos assíncronos chamados do fluxo da interface do usuário não precisam empacotar manualmente as chamadas de volta para o fluxo da interface do usuário para atualizar os controles da interface do usuário: o Framework executa essa empacotamento automaticamente.
Infelizmente, esse empacotamento tem um preço. Para desenvolvedores de aplicativos que aguardam para implementar seu fluxo de controle, o empacotamento automático é a solução certa. As bibliotecas geralmente têm uma história completamente diferente. Para desenvolvedores de aplicativos, esse empacotamento é principalmente necessário para o código controlar o contexto em que é executado, por exemplo, para acessar os controles da interface do usuário ou para acessar o HttpContext correspondente à solicitação do ASP.NET necessária. No entanto, as bibliotecas geralmente não são necessárias para atender a esse requisito. Como resultado, o empacotamento automático geralmente gera custos adicionais completamente desnecessários. Vamos dar uma nova olhada no código que copia dados de um fluxo para outro:
byte [] buffer = new byte[0x1000]; int numRead; while((numRead = await source.ReadAsync(buffer, 0, buffer.Length)) > 0) { await source.WriteAsync(buffer, 0, numRead); }
Se essa cópia for chamada do fluxo da interface do usuário, cada operação de leitura e gravação forçará a execução a retornar ao fluxo da interface do usuário. No caso de um megabyte de dados na origem e nos fluxos que lêem e gravam de forma assíncrona (ou seja, a maioria de suas implementações), isso significa cerca de 500 opções do fluxo de segundo plano para o fluxo da interface do usuário. Para lidar com esse comportamento nos tipos de tarefas e tarefas, o método ConfigureAwait é criado. Este método aceita o parâmetro continueOnCapturedContext de um tipo booleano que controla empacotamento. Se true (o padrão), aguardar retornará automaticamente o controle para o SynchronizationContext capturado. Se false for usado, o contexto de sincronização será ignorado e o ambiente continuará executando a operação assíncrona no encadeamento em que foi interrompido. A implementação dessa lógica fornecerá uma versão mais eficiente do código de cópia entre os threads:
byte [] buffer = new byte[0x1000]; int numRead; while((numRead = await source.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false)) > 0) { await source.WriteAsync(buffer, 0, numRead).ConfigureAwait(false); }
Para desenvolvedores de bibliotecas, essa aceleração por si só é suficiente para sempre pensar em usar o ConfigureAwait, com exceção de raras condições nas quais a biblioteca conhece o tempo de execução e precisará executar o método com acesso ao contexto correto.
Além do desempenho, há outro motivo pelo qual você precisa usar o ConfigureAwait ao desenvolver bibliotecas. Imagine que o método CopyStreamToStreamAsync, implementado com uma versão do código sem ConfigureAwait, seja chamado de um fluxo de UI no WPF, por exemplo, assim:
private void button1_Click(object sender, EventArgs args) { Stream src = …, dst = …; Task t = CopyStreamToStreamAsync(src, dst); t.Wait(); // deadlock! }
Nesse caso, o programador precisou escrever button1_Click como um método assíncrono no qual o operador de espera espera executar a tarefa e não usar o método de espera síncrona desse objeto. O método Wait precisa ser usado em muitos outros casos, mas quase sempre será um erro usá-lo para aguardar em um fluxo da interface do usuário, como mostrado aqui. O método Wait não retornará até que a tarefa seja concluída. No caso de CopyStreamToStreamAsync, seu fluxo assíncrono tenta retornar a execução enviando dados para o SynchronizationContext capturado e não pode concluir até que essas transferências sejam concluídas (porque são necessárias para continuar sua operação). Mas esses despachos, por sua vez, não podem ser executados, porque o thread da interface do usuário que deve manipulá-los é bloqueado pela chamada Aguardar. Essa é uma dependência cíclica que leva a um impasse. Se CopyStreamToStreamAsync for implementado com ConfigureAwait (false), não haverá dependência e bloqueio.
ExecutionContext ExecutionContext é uma parte importante do .NET Framework, mas ainda assim muitos programadores desconhecem sua existência. ExecutionContext – , SecurityContext LogicalCallContext, , . , ThreadPool.QueueUserWorkItem, Task.Run, Delegate.BeginInvoke, Stream.BeginRead, WebClient.DownloadStringAsync Framework, ExecutionContext ExecutionContext.Run ( ). , , ThreadPool.QueueUserWorkItem, Windows (identity), WaitCallback. , Task.Run LogicalCallContext, LogicalCallContext Action. ExecutionContext .
Framework , ExecutionContext, , . Windows LogicalCallContext . (WindowsIdentity.Impersonate CallContext.LogicalSetData) .
. C# Visual Basic , . await. , , - . C# Visual Basic («») , await (boxed) , .
. , . , , , .
C# Visual Basic , . ,
public static async Task FooAsync() { var dto = DateTimeOffset.Now; var dt = dto.DateTime; await Task.Yield(); Console.WriteLine(dt); }
dto await, . , , - dto:
Figure 4
[StructLayout(LayoutKind.Sequential), CompilerGenerated] private struct <FooAsync>d__0 : <>t__IStateMachine { private int <>1__state; public AsyncTaskMethodBuilder <>t__builder; public Action <>t__MoveNextDelegate; public DateTimeOffset <dto>5__1; public DateTime <dt>5__2; private object <>t__stack; private object <>t__awaiter; public void MoveNext(); [DebuggerHidden] public void <>t__SetMoveNextDelegate(Action param0); }
, . , , , , . , :
public static async Task FooAsync() { var dt = DateTimeOffset.Now.DateTime; await Task.Yield(); Console.WriteLine(dt); }
, .NET (GC) , , , : 0, , , (.NET GC 0, 1 2). , GC . , , , , , , . 0, , , . , , , .
( , ). JIT , , , , . , , . , , , , . , , . , C# Visual Basic , , .
C# Visual Basic , awaits: . await , Task , , . , , :
public static async Task<int> SumAsync(Task<int> a, Task<int> b, Task<int> c) { return Sum(await a, await b, await c); } private static int Sum(int a, int b, int c) { return a + b + c; }
C# “await b” Sum. await, Sum, - async , «» await. , await . , , CLR, , , . , <>t__stack. , , Tuple<int, int> <>__stack. , , , . , SumAsync :
public static async Task<int> SumAsync(Task<int> a, Task<int> b, Task<int> c) { int ra = await a; int rb = await b; int rc = await c; return Sum(ra, rb, rc); }
, ra, rb rc, . , : . , , , . , , , , .
, , . Sum , await , . , await , . await , Task.WhenAll:
public static async Task<int> SumAsync(Task<int> a, Task<int> b, Task<int> c) { int [] results = await Task.WhenAll(a, b, c); return Sum(results[0], results[1], results[2]); }
Task.WhenAll Task<TResult[]>, , , , . . , WhenAll, Task Task. , , , , , WhenAll , . WhenAll, , , params, . , , . Figure 5
Figure 5
public static Task<int> SumAsync(Task<int> a, Task<int> b, Task<int> c) { return (a.Status == TaskStatus.RanToCompletion && b.Status == TaskStatus.RanToCompletion && c.Status == TaskStatus.RanToCompletion) ? Task.FromResult(Sum(a.Result, b.Result, c.Result)) : SumAsyncInternal(a, b, c); } private static async Task<int> SumAsyncInternal(Task<int> a, Task<int> b, Task<int> c) { await Task.WhenAll((Task)a, b, c).ConfigureAwait(false); return Sum(a.Result, b.Result, c.Result); }
, . , . , . , , : , , / , . .NET Framework , . , .NET Framework, . , , Framework, , , .