A funcionalidade Async / Await foi introduzida no C # 5 para melhorar a capacidade de resposta da interface do usuário e o acesso da Web aos recursos. Em outras palavras, métodos assíncronos ajudam os desenvolvedores a executar operações assíncronas que não bloqueiam threads e retornam um único resultado escalar. Após inúmeras tentativas da Microsoft de simplificar operações assíncronas, o modelo async / waitit ganhou uma boa reputação entre os desenvolvedores, graças a uma abordagem simples.
Os métodos assíncronos existentes são significativamente limitados, pois devem retornar apenas um valor. Vejamos algum método async Task<int> DoAnythingAsync()
que é comum para essa sintaxe. O resultado de seu trabalho é um significado único. Devido a essa limitação, você não pode usar esta função com a palavra-chave yield
e a IEnumerable<int>
assíncrona IEnumerable<int>
(para retornar o resultado de uma enumeração assíncrona).

Se você combinar a função async/await
e a yield
, poderá usar um poderoso modelo de programação conhecido como extração de dados assíncronos ou uma enumeração baseada em extração ou uma sequência assíncrona assíncrona , como é chamado em F #.
A nova capacidade de usar segmentos assíncronos no C # 8 remove a limitação associada ao retorno de um único resultado e permite que o método assíncrono retorne vários valores. Essas alterações darão ao modelo assíncrono mais flexibilidade e o usuário poderá recuperar dados de algum lugar (por exemplo, do banco de dados) usando sequências assíncronas atrasadas ou receber dados de sequências assíncronas em partes, conforme disponível.
Um exemplo:
foreach await (var streamChunck in asyncStreams) { Console.WriteLine($“Received data count = {streamChunck.Count}”); }
Outra abordagem para resolver problemas relacionados à programação assíncrona é usar extensões reativas (Rx). Rx
está ganhando importância entre os desenvolvedores e esse método é usado em muitas linguagens de programação, por exemplo, Java (RxJava) e JavaScript (RxJS).
O Rx é baseado em um modelo push push (princípio Tell Don't Ask), também conhecido como programação reativa. I.e. diferentemente de IEnumerable, quando o consumidor solicita o próximo elemento, no modelo Rx, o provedor de dados sinaliza ao consumidor que um novo elemento aparece na sequência. Os dados são enviados para a fila no modo assíncrono e o consumidor os utiliza no momento do recebimento.
Neste artigo, compararei um modelo baseado em dados por envio (como Rx) com um modelo baseado em dados por recebimento (como IEnumerable) e também mostrarei quais cenários são mais adequados para cada modelo. Todo o conceito e os benefícios são examinados com uma variedade de exemplos e código de demonstração. No final, mostrarei o aplicativo e o demonstrarei com um exemplo de código.
Comparação de um modelo baseado em envio de dados com um modelo baseado em recebimento de dados (recebimento)

Fig. -1- Comparação de um modelo baseado na extração de dados com um modelo baseado na extração de dados
Esses exemplos são baseados no relacionamento entre o provedor de dados e o consumidor, conforme mostrado na Fig. -1-. Um modelo baseado em pull é fácil de entender. Nele, o consumidor solicita e recebe dados do fornecedor. Uma abordagem alternativa é um modelo push push. Aqui, o provedor publica os dados na fila e o consumidor deve se inscrever para recebê-los.
O modelo de coleta de dados é adequado para casos em que o provedor gera dados mais rapidamente do que o consumidor os utiliza. Assim, o consumidor recebe apenas os dados necessários, o que evita problemas de transbordamento. Se o consumidor usa os dados mais rapidamente do que o fornecedor os produz, um modelo baseado em enviar os dados é adequado. Nesse caso, o fornecedor pode enviar mais dados ao consumidor para que não haja atrasos desnecessários.
Rx e Akka Streams (um modelo de programação baseado em fluxo) usam o método de contrapressão para controlar o fluxo. Para resolver os problemas do fornecedor e do destinatário descritos acima, o método usa os dados de envio e recebimento.
No exemplo abaixo, um consumidor lento extrai dados de um provedor mais rápido. Depois que o consumidor processar o elemento atual, ele solicitará ao fornecedor o próximo e assim por diante até o final da sequência.
Para entender toda a necessidade de threads assíncronos, considere o código a seguir.
// (count) static int SumFromOneToCount(int count) { ConsoleExt.WriteLine("SumFromOneToCount called!"); var sum = 0; for (var i = 0; i <= count; i++) { sum = sum + i; } return sum; } // : const int count = 5; ConsoleExt.WriteLine($"Starting the application with count: {count}!"); ConsoleExt.WriteLine("Classic sum starting."); ConsoleExt.WriteLine($"Classic sum result: {SumFromOneToCount(count)}"); ConsoleExt.WriteLine("Classic sum completed."); ConsoleExt.WriteLine("################################################"); ConsoleExt.WriteLine(Environment.NewLine);
Conclusão:

Podemos adiar o método usando a declaração de rendimento, como mostrado abaixo.
static IEnumerable<int> SumFromOneToCountYield(int count) { ConsoleExt.WriteLine("SumFromOneToCountYield called!"); var sum = 0; for (var i = 0; i <= count; i++) { sum = sum + i; yield return sum; } }
Chamada de método
const int count = 5; ConsoleExt.WriteLine("Sum with yield starting."); foreach (var i in SumFromOneToCountYield(count)) { ConsoleExt.WriteLine($"Yield sum: {i}"); } ConsoleExt.WriteLine("Sum with yield completed."); ConsoleExt.WriteLine("################################################"); ConsoleExt.WriteLine(Environment.NewLine);
Conclusão:

Como mostrado na janela de saída acima, o resultado é retornado em partes, não em um único valor. Os resultados resumidos mostrados acima são conhecidos como listagem adiada. No entanto, o problema ainda não foi resolvido: os métodos de soma bloqueiam o código. Se você olhar para os threads, poderá ver que tudo está sendo executado no thread principal.
Vamos aplicar a palavra mágica assíncrona ao primeiro método SumFromOneToCount (sem rendimento).
static async Task<int> SumFromOneToCountAsync(int count) { ConsoleExt.WriteLine("SumFromOneToCountAsync called!"); var result = await Task.Run(() => { var sum = 0; for (var i = 0; i <= count; i++) { sum = sum + i; } return sum; }); return result; }
Chamada de método
const int count = 5; ConsoleExt.WriteLine("async example starting."); // . , . , . var result = await SumFromOneToCountAsync(count); ConsoleExt.WriteLine("async Result: " + result); ConsoleExt.WriteLine("async completed."); ConsoleExt.WriteLine("################################################"); ConsoleExt.WriteLine(Environment.NewLine);
Conclusão:

Ótimo. Agora os cálculos são feitos em um encadeamento diferente, mas o problema com o resultado ainda existe. O sistema retorna o resultado com um único valor.
Imagine que podemos combinar enumerações diferidas (declaração de rendimento) e métodos assíncronos em um estilo de programação imperativa. A combinação é chamada de fluxos assíncronos e esse é um novo recurso do C # 8. É ótimo para resolver problemas associados a um modelo de programação baseado na extração de dados, por exemplo, baixando dados de um site ou lendo registros em um arquivo ou banco de dados de maneiras modernas.
Vamos tentar fazer isso na versão atual do C #. Vou adicionar a palavra-chave async ao método SumFromOneToCountYield da seguinte maneira:

Fig. -2- Erro ao usar a palavra-chave yield e async ao mesmo tempo.
Quando tentamos adicionar assíncrono ao SumFromOneToCountYield, ocorre um erro como mostrado acima.
Vamos tentar de forma diferente. Podemos remover a palavra-chave yield e aplicar IEnumerable na tarefa, conforme mostrado abaixo:
static async Task<IEnumerable<int>> SumFromOneToCountTaskIEnumerable(int count) { ConsoleExt.WriteLine("SumFromOneToCountAsyncIEnumerable called!"); var collection = new Collection<int>(); var result = await Task.Run(() => { var sum = 0; for (var i = 0; i <= count; i++) { sum = sum + i; collection.Add(sum); } return collection; }); return result; }
Chamada de método
const int count = 5; ConsoleExt.WriteLine("SumFromOneToCountAsyncIEnumerable started!"); var scs = await SumFromOneToCountTaskIEnumerable(count); ConsoleExt.WriteLine("SumFromOneToCountAsyncIEnumerable done!"); foreach (var sc in scs) { // , . . ConsoleExt.WriteLine($"AsyncIEnumerable Result: {sc}"); } ConsoleExt.WriteLine("################################################"); ConsoleExt.WriteLine(Environment.NewLine);
Conclusão:

Como você pode ver no exemplo, tudo é calculado no modo assíncrono, mas o problema ainda permanece. Os resultados (todos os resultados são coletados em uma coleção) são retornados como um único bloco. E não é disso que precisamos. Se você se lembra, nosso objetivo era combinar o modo de cálculo assíncrono com a possibilidade de atraso.
Para fazer isso, você precisa usar uma biblioteca externa, por exemplo, Ix (parte de Rx) ou threads assíncronos, apresentados em C #.
Vamos voltar ao nosso código. Para demonstrar comportamento assíncrono, usei uma biblioteca externa .
static async Task ConsumeAsyncSumSeqeunc(IAsyncEnumerable<int> sequence) { ConsoleExt.WriteLineAsync("ConsumeAsyncSumSeqeunc Called"); await sequence.ForEachAsync(value => { ConsoleExt.WriteLineAsync($"Consuming the value: {value}"); // Task.Delay(TimeSpan.FromSeconds(1)).Wait(); }); } static IEnumerable<int> ProduceAsyncSumSeqeunc(int count) { ConsoleExt.WriteLineAsync("ProduceAsyncSumSeqeunc Called"); var sum = 0; for (var i = 0; i <= count; i++) { sum = sum + i; // Task.Delay(TimeSpan.FromSeconds(0,5)).Wait(); yield return sum; } }
Chamada de método
const int count = 5; ConsoleExt.WriteLine("Starting Async Streams Demo!"); // . . IAsyncEnumerable<int> pullBasedAsyncSequence = ProduceAsyncSumSeqeunc(count).ToAsyncEnumerable(); ConsoleExt.WriteLineAsync("X#X#X#X#X#X#X#X#X#X# Doing some other work X#X#X#X#X#X#X#X#X#X#"); // ; . var consumingTask = Task.Run(() => ConsumeAsyncSumSeqeunc(pullBasedAsyncSequence)); // . , . consumingTask.Wait(); ConsoleExt.WriteLineAsync("Async Streams Demo Done!");
Conclusão:

Finalmente, vemos o comportamento desejado. Você pode executar um loop de enumeração no modo assíncrono.
Veja o código fonte aqui .
Extração assíncrona de dados usando a arquitetura cliente-servidor como exemplo
Vejamos esse conceito com um exemplo mais realista. Todos os benefícios desse recurso são vistos melhor no contexto da arquitetura cliente-servidor.
Chamada síncrona no caso da arquitetura cliente-servidor
Ao enviar uma solicitação ao servidor, o cliente é forçado a esperar (ou seja, está bloqueado) até que uma resposta chegue, como mostra a Fig. -3-.

Fig. -3- Extração de dados síncronos, durante a qual o cliente espera até que o processamento da solicitação seja concluído
Extração de dados assíncrona
Nesse caso, o cliente solicita dados e passa para outras tarefas. Depois que os dados são recebidos, o cliente continuará fazendo o trabalho.

Fig. -4- Extração assíncrona de dados, durante a qual o cliente pode executar outras tarefas enquanto os dados estão sendo solicitados
Puxando dados de forma assíncrona
Nesse caso, o cliente solicita uma parte dos dados e continua a executar outras tarefas. Depois de receber os dados, o cliente os processa e solicita a próxima parte, e assim por diante, até que todos os dados sejam recebidos. Foi a partir desse cenário que surgiu a idéia de encadeamentos assíncronos. Na fig. -5- mostra como o cliente pode processar os dados recebidos ou executar outras tarefas.

Fig. -5- Puxando dados como uma sequência assíncrona (fluxos assíncronos). O cliente não está bloqueado.
Threads assíncronos
Como IEnumerable<T>
e IEnumerator<T>
existem duas novas interfaces, IAsyncEnumerable<T>
e IAsyncEnumerator<T>
, definidas conforme mostrado abaixo:
public interface IAsyncEnumerable<out T> { IAsyncEnumerator<T> GetAsyncEnumerator(); } public interface IAsyncEnumerator<out T> : IAsyncDisposable { Task<bool> MoveNextAsync(); T Current { get; } } // public interface IAsyncDisposable { Task DiskposeAsync(); }
No InfoQ, Jonathan Allen acertou este tópico. Aqui não vou entrar em detalhes, por isso recomendo a leitura do artigo dele .
O foco está no valor de retorno da Task<bool> MoveNextAsync()
(alterada de bool para Task<bool>
, bool IEnumerator.MoveNext()
). Graças a ele, todos os cálculos, bem como a iteração, ocorrerão de forma assíncrona. O consumidor decide quando obter o próximo valor. Embora seja um modelo assíncrono, ele ainda usa extração de dados. Para limpeza assíncrona de recursos, você pode usar a interface IAsyncDisposable
. Mais informações sobre threads assíncronos podem ser encontradas aqui .
Sintaxe
A sintaxe final deve se parecer com o seguinte:
foreach await (var dataChunk in asyncStreams) { // yield . }
A partir do exemplo acima, fica claro que, em vez de calcular um único valor, teoricamente, podemos calcular sequencialmente um conjunto de valores, enquanto aguardamos outras operações assíncronas.
Exemplo redesenhado da Microsoft
Reescrevi o código de demonstração da Microsoft. Ele pode ser baixado inteiramente do meu repositório GitHub .
O exemplo é baseado na idéia de criar um grande fluxo na memória (uma matriz de 20.000 bytes) e extrair sequencialmente elementos dele no modo assíncrono. Durante cada iteração, 8 KB são extraídos da matriz.


Na etapa (1), uma grande matriz de dados é criada, preenchida com valores fictícios. Então, durante a etapa (2), uma variável chamada soma de verificação é definida. Essa variável que contém a soma de verificação destina-se a verificar a exatidão da soma dos cálculos. Uma matriz e uma soma de verificação são criadas na memória e retornadas como uma sequência de elementos na etapa (3).
A etapa (4) envolve a aplicação do AsEnumarble
extensão AsEnumarble
(um nome mais adequado AsAsyncEnumarble), que ajuda a simular um fluxo assíncrono de 8 KB (BufferSize = 8000 elementos (6))
Herdar de IAsyncEnumerable geralmente não é necessário, mas no exemplo mostrado acima, esta operação é executada para simplificar o código de demonstração, conforme mostrado na etapa (5).
A etapa (7) envolve o uso da palavra-chave foreach
, que extrai pedaços de 8 KB de dados de um fluxo assíncrono na memória. O processo de extração ocorre sequencialmente: quando o consumidor (parte do código que contém foreach
) está pronto para receber os próximos dados, ele os extrai do provedor (a matriz contida no fluxo na memória). Por fim, quando o ciclo estiver concluído, o programa verificará o valor de 'c' para a soma de verificação e, se corresponderem, exibirá a mensagem “Checksums match!”, Conforme a etapa (8).
Janela de saída de demonstração da Microsoft:

Conclusão
Analisamos segmentos assíncronos, ótimos para extrair dados de forma assíncrona e escrever código que gera vários valores no modo assíncrono.
Usando esse modelo, você pode consultar o próximo elemento de dados em uma sequência e obter uma resposta. Difere do modelo de envio de dados IObservable<T>
, usando quais valores são gerados independentemente do estado do consumidor. Fluxos assíncronos permitem representar perfeitamente fontes de dados assíncronas controladas pelo consumidor quando ele próprio determina a disposição de aceitar a próxima parte de dados. Os exemplos incluem o uso de aplicativos da web ou a leitura de registros em um banco de dados.
Demonstrei como criar uma enumeração no modo assíncrono e usá-lo usando uma biblioteca externa com sequência assíncrona. Também mostrei quais benefícios esse recurso oferece ao baixar conteúdo da Internet. Por fim, analisamos a nova sintaxe para encadeamentos assíncronos, bem como um exemplo completo de seu uso baseado no Microsoft Build Demo Code ( 7 a 9 de maio de 2018 // Seattle, WA )
