Bom dia Desta vez, falaremos sobre um tópico que todo aderente que se preze da linguagem C # começou a entender - programação assíncrona usando Task ou, em pessoas comuns, assíncrona / aguardada. A Microsoft fez um bom trabalho - para usar a assincronia na maioria dos casos, você só precisa conhecer a sintaxe e nenhum outro detalhe. Mas se você se aprofundar, o tópico é bastante volumoso e complexo. Foi afirmado por muitos, cada um em seu próprio estilo. Existem muitos artigos interessantes sobre esse tópico, mas ainda existem muitos conceitos errados. Vamos tentar corrigir a situação e mastigar o material o máximo possível, sem sacrificar a profundidade ou a compreensão.

Tópicos / capítulos cobertos:
- O conceito de assincronia - os benefícios da assincronia e dos mitos sobre um fio "bloqueado"
- TAP. Sintaxe e condições de compilação - pré-requisitos para escrever um método de compilação
- Trabalhar com o uso da TAP - a mecânica e o comportamento do programa em código assíncrono (liberando threads, iniciando tarefas e aguardando a conclusão)
- Nos bastidores: a máquina de estados - uma visão geral das transformações do compilador e as classes que gera
- As origens da assincronia. O dispositivo de métodos assíncronos padrão - métodos assíncronos para trabalhar com arquivos e a rede por dentro
- Classes e truques TAP são truques úteis que podem ajudá-lo a gerenciar e acelerar um programa usando o TAP
Conceito assíncrono
A assincronia por si só está longe de ser nova. A assincronia geralmente implica em executar uma operação em um estilo que não implique em bloquear o encadeamento de chamada, ou seja, iniciar a operação sem aguardar sua conclusão. O bloqueio não é tão mau como é descrito. Pode-se encontrar afirmações de que threads bloqueados perdem tempo de CPU, trabalham mais lentamente e causam chuva. Parece este último improvável? De fato, os 2 pontos anteriores são os mesmos.
No nível do planejador do SO, quando um encadeamento está em um estado "bloqueado", um tempo precioso do processador não será alocado a ele. As chamadas do agendador, em regra, recaem sobre operações que causam bloqueio, interrupções no cronômetro e outras interrupções. Ou seja, quando, por exemplo, o controlador de disco conclui a operação de leitura e inicia uma interrupção apropriada, o agendador inicia. Ele decidirá se deve iniciar um encadeamento que foi bloqueado por esta operação ou outro com prioridade mais alta.
O trabalho lento parece ainda mais absurdo. De fato, o trabalho é o mesmo. Somente a operação assíncrona adicionará um pouco mais de sobrecarga.
O desafio da chuva geralmente não é algo desta área.
O principal problema de bloqueio é o consumo não razoável de recursos do computador. Mesmo se esquecermos o tempo para criar um encadeamento e trabalhar com um conjunto de encadeamentos, cada encadeamento bloqueado consumirá espaço extra. Bem, há cenários em que apenas um thread pode executar determinado trabalho (por exemplo, um thread da interface do usuário). Consequentemente, eu não gostaria que ele estivesse ocupado com uma tarefa que outro encadeamento possa executar, sacrificando o desempenho de operações exclusivas a ele.
A assincronia é um conceito muito amplo e pode ser alcançado de várias maneiras.
O seguinte pode ser diferenciado no histórico do .NET :
- EAP (padrão assíncrono baseado em evento) - como o nome indica, a caminhada é baseada em eventos que são acionados quando a operação é concluída e no método usual que chama essa operação
- APM (Modelo de Programação Assíncrona) - baseado em 2 métodos. O método BeginSmth retorna a interface IAsyncResult. O método EndSmth aceita IAsyncResult (se a operação não for concluída no momento em que EndSmth for chamado, o encadeamento será bloqueado)
- TAP (padrão assíncrono baseado em tarefas) é o mesmo assíncrono / espera (estritamente falando, essas palavras apareceram após a abordagem e os tipos tarefa e tarefa <TResult> apareceram, mas async / wait melhorou significativamente esse conceito)
A última abordagem foi tão bem-sucedida que todos se esqueceram das anteriores. Então, será sobre ele.
Padrão assíncrono baseado em tarefas. Condições de sintaxe e compilação
O método assíncrono padrão do estilo TAP é muito fácil de escrever.
Para fazer isso, você precisa :
- Para que o valor de retorno seja Tarefa, Tarefa <T> ou nula (não recomendado, discutido posteriormente). No C # 7, surgiram os tipos de tarefas (discutidos no capítulo anterior). No C # 8, IAsyncEnumerable <T> e IAsyncEnumerator <T> são adicionados a esta lista.
- Para que o método seja marcado com a palavra-chave assíncrona e contenha aguardar dentro. Essas palavras-chave estão emparelhadas. Além disso, se o método contiver aguardar, marque-o como assíncrono, o contrário não é verdadeiro, mas é inútil
- Por decência, respeite a convenção de sufixo Async. Obviamente, o compilador não considerará isso um erro. Se você é um desenvolvedor decente, pode adicionar sobrecargas com um CancellationToken (discutido no capítulo anterior)
Para esses métodos, o compilador faz um trabalho sério. E eles se tornam completamente irreconhecíveis nos bastidores, mas mais sobre isso mais tarde.
Foi mencionado que o método deve conter a palavra-chave wait. Ele (a palavra) indica a necessidade de espera assíncrona para a tarefa ser executada, que é o objeto da tarefa ao qual é aplicada.
O objeto de tarefa também possui determinadas condições para que o aguardar possa ser aplicado a ele:- O tipo esperado deve ter um método público (ou interno) GetAwaiter (), também pode ser um método de extensão. Este método retorna um objeto de espera.
- O objeto de espera deve implementar a interface INotifyCompletion, que requer a implementação do método nulo OnCompleted (continuação da ação). Também deve ter a propriedade da instância bool IsCompleted, o método GetResult () nulo. Pode ser uma estrutura ou uma classe.
O exemplo abaixo mostra como fazer um int esperado, e mesmo nunca executado.
Extensão intpublic class Program { public static async Task Main() { await 1; } } public static class WeirdExtensions { public static AnyTypeAwaiter GetAwaiter(this int number) => new AnyTypeAwaiter(); public class AnyTypeAwaiter : INotifyCompletion { public bool IsCompleted => false; public void OnCompleted(Action continuation) { } public void GetResult() { } } }
Trabalhar com TAP
É difícil entrar na selva sem entender como algo deve funcionar. Considere a TAP em termos de comportamento do programa.
Em terminologia: o método assíncrono em questão, cujo código será considerado, chamarei o
método assíncrono e os métodos assíncronos chamados dentro dele, chamarei a
operação assíncrona .
Vamos dar o exemplo mais simples: como operação assíncrona, usamos Task.Delay, que atrasa o tempo especificado sem bloquear o fluxo.
public static async Task DelayOperationAsync()
A execução do método em termos de comportamento é a seguinte.
- Todo o código que precede a chamada da operação assíncrona é executado. Nesse caso, esse é o método BeforeCall
- Uma chamada de operação assíncrona está em andamento. Nesta fase, o encadeamento não é liberado ou bloqueado. Esta operação retorna o resultado - o objeto de tarefa mencionado (geralmente Tarefa), que é armazenado em uma variável local
- O código é executado depois de chamar a operação assíncrona, mas antes de esperar (aguardar). No exemplo - AfterCall
- Aguardando conclusão no objeto de tarefa (que é armazenado em uma variável local) - aguarda tarefa.
Se a operação assíncrona for concluída neste momento, a execução continuará sincronizada, no mesmo encadeamento.
Se a operação assíncrona não for concluída, será salvo o código que deve ser chamado após a conclusão da operação assíncrona (a chamada continuação) e o fluxo retornará ao conjunto de encadeamentos e ficará disponível para uso. - A execução das operações após a espera - AfterAwait - é executada imediatamente, no mesmo encadeamento, quando a operação no momento da espera foi concluída ou, após a conclusão da operação, é realizado um novo encadeamento que continuará (salvo na etapa anterior)
Nos bastidores. Máquina de estado
De fato, nosso método é transformado pelo compilador em um método stub no qual a classe gerada - a máquina de estado - é inicializada. Em seguida, ele (a máquina) inicia e o objeto Task usado na etapa 2 é retornado do método.
De particular interesse é o método
MoveNext da máquina de estado. Este método faz o que era antes da conversão no método assíncrono. Ele quebra o código entre cada chamada em espera. Cada peça é realizada em uma determinada condição da máquina. O próprio método
MoveNext é anexado ao objeto de espera como uma continuação. A preservação do estado garante a execução precisamente daquela parte que seguiu logicamente a expectativa.
Como se costuma dizer, é melhor ver 1 vez do que ouvir 100 vezes, por isso recomendo que você se familiarize com o exemplo abaixo. Reescrevi o código um pouco, aprimorei a nomeação de variáveis e comentei generosamente.
Código fonte public static async Task Delays() { Console.WriteLine(1); await Task.Delay(1000); Console.WriteLine(2); await Task.Delay(1000); Console.WriteLine(3); await Task.Delay(1000); Console.WriteLine(4); await Task.Delay(1000); Console.WriteLine(5); await Task.Delay(1000); }
Método de esboço [AsyncStateMachine(typeof(DelaysStateMachine))] [DebuggerStepThrough] public Task Delays() { DelaysStateMachine stateMachine = new DelaysStateMachine(); stateMachine.taskMethodBuilder = AsyncTaskMethodBuilder.Create(); stateMachine.currentState = -1; AsyncTaskMethodBuilder builder = stateMachine.taskMethodBuilder; taskMethodBuilder.Start(ref stateMachine); return stateMachine.taskMethodBuilder.Task; }
Máquina de estado [CompilerGenerated] private sealed class DelaysStateMachine : IAsyncStateMachine {
Eu me concentro na frase "neste momento não foi executado de forma síncrona". Uma operação assíncrona também pode seguir um caminho de execução síncrona. A principal condição para o atual método assíncrono a ser executado de forma síncrona, ou seja, sem alterar o encadeamento, é a conclusão da operação assíncrona no momento da verificação
IsCompleted .
Este exemplo demonstra claramente esse comportamento. static async Task Main() { Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
Sobre o contexto de sincronização. O método
AwaitUnsafeOnCompleted usado na máquina resulta em uma chamada para o método
Task.SetContinuationForAwait . Nesse método, o contexto de sincronização atual
SynchronizationContext.Current é recuperado. O contexto de sincronização pode ser interpretado como um tipo de fluxo. Se também for específico (por exemplo, o contexto do encadeamento da interface do usuário), uma continuação será criada usando a classe
SynchronizationContextAwaitTaskContinuation . Essa classe para iniciar a continuação chama o método Post no contexto salvo, o que garante que a continuação seja executada no contexto exato em que o método foi executado. A lógica específica para executar a continuação depende do método
Post em um contexto que, para dizer o mínimo, não é conhecido pela velocidade. Se não houve contexto de sincronização (ou foi indicado que não nos importa em que contexto a execução continuará usando o ConfigureAwait (false), que será discutido no capítulo anterior), a continuação será realizada pelo encadeamento do pool.
As origens da assincronia. Os métodos assíncronos padrão do dispositivo
Vimos como um método usando assíncrono e aguarda a aparência e o que acontece nos bastidores. Esta informação não é incomum. Mas é importante entender a natureza das operações assíncronas. Porque, como vimos na máquina de estado, operações assíncronas são chamadas no código, a menos que o resultado seja processado de maneira mais esperta. No entanto, o que acontece dentro das próprias operações assíncronas? Provavelmente o mesmo, mas isso não pode acontecer ad infinitum.
Uma tarefa importante é entender a natureza da assincronia. Ao tentar entender a assincronia, há uma alternância de estados "agora claros" e "agora novamente incompreensíveis". E essa alternância ocorrerá até que a fonte da assincronia seja entendida.
Ao trabalhar com assincronia, operamos em tarefas. Isso não é o mesmo que um fluxo. Uma tarefa pode ser executada por vários threads, e um thread pode executar muitas tarefas.
A assincronia geralmente começa com um método que retorna a tarefa (por exemplo), mas não é marcada com async e, portanto, não usa aguardar por dentro. Este método não tolera nenhuma alteração do compilador, é executado como está.
Então, vamos olhar para algumas das raízes da assincronia.- Task.Run, new Task (..). Start (), Factory.StartNew e similares. A maneira mais fácil de iniciar a execução assíncrona. Esses métodos simplesmente criam um novo objeto de tarefa, passando um delegado como um dos parâmetros. A tarefa é transferida para o planejador, o que permite que ela seja executada por um dos encadeamentos no conjunto. A tarefa finalizada que pode ser esperada é retornada. Normalmente, essa abordagem é usada para iniciar a computação (ligada à CPU) em um encadeamento separado.
- TaskCompletionSource. Uma classe auxiliar que ajuda a controlar o objeto da tarefa. Projetado para aqueles que não podem alocar um delegado para implementação e usa mecanismos mais sofisticados para controlar a conclusão. Possui uma API muito simples - SetResult, SetError, etc., que atualiza a tarefa de acordo. Esta tarefa está disponível através da propriedade Task. Talvez por dentro você crie threads, tenha uma lógica complexa para sua interação ou conclusão por evento. Um pouco mais de detalhes sobre essa classe estarão na última seção.
Em um parágrafo adicional, você pode criar os métodos de bibliotecas padrão. Isso inclui arquivos de leitura / gravação, trabalho com uma rede e similares. Como regra, esses métodos populares e comuns usam chamadas de sistema que variam em plataformas diferentes, e seu dispositivo é extremamente divertido. Considere trabalhar com arquivos e a rede.
Arquivos
Uma observação importante - se você quiser trabalhar com arquivos, deverá especificar useAsync = true ao criar o FileStream.
Tudo é organizado em arquivos de maneira não trivial e confusa. A classe FileStream é declarada como parcial. Além disso, existem mais 6 complementos específicos da plataforma. Portanto, no Unix, o acesso assíncrono a um arquivo arbitrário, como regra, usa uma operação síncrona em um encadeamento separado. No Windows, há chamadas de sistema para operação assíncrona, as quais, é claro, são usadas. Isso leva a diferenças no trabalho em diferentes plataformas.
Fontes .
UnixO comportamento padrão ao escrever ou ler é executar a operação de forma síncrona, se o buffer permitir e o fluxo não estiver ocupado com outra operação:
1. O fluxo não está ocupado com outra operação
A classe Filestream possui um objeto herdado de SemaphoreSlim com os parâmetros (1, 1) - ou seja, uma seção crítica - o fragmento de código protegido por esse semáforo pode ser executado por apenas um encadeamento por vez. Esse semáforo é usado tanto para leitura quanto para escrita. Ou seja, é impossível produzir simultaneamente leitura e escrita. Nesse caso, o bloqueio no semáforo não ocorre. O método this._asyncState.WaitAsync () é chamado nele, que retorna o objeto da tarefa (não há bloqueio ou espera, seria se a palavra-chave wait for aplicada ao resultado do método). Se esse objeto de tarefa não for concluído - ou seja, o semáforo for capturado, a continuação (Task.ContinueWith) na qual a operação é executada será anexada ao objeto de espera retornado. Se o objeto estiver livre, você precisará verificar o seguinte
2. O buffer permite
Aqui o comportamento já depende da natureza da operação.
Para gravação - verifica-se que o tamanho dos dados para escrever + posição no arquivo é menor que o tamanho do buffer, que por padrão é 4096 bytes. Ou seja, devemos escrever 4096 bytes desde o início, 2048 bytes com um deslocamento de 2048 e assim por diante. Se for esse o caso, a operação é realizada de forma síncrona, caso contrário, a continuação é anexada (Task.ContinueWith). A sequela usa uma chamada síncrona regular do sistema. Quando o buffer está cheio, ele é gravado no disco de forma síncrona.
Para leitura - é verificado se há dados suficientes no buffer para retornar todos os dados necessários. Caso contrário, novamente, uma continuação (Task.ContinueWith) com uma chamada de sistema síncrona.
A propósito, há um detalhe interessante. Se um dado ocupar o buffer inteiro, eles serão gravados diretamente no arquivo, sem a participação do buffer. Ao mesmo tempo, há uma situação em que haverá mais dados do que o tamanho do buffer, mas todos passarão por ele. Isso acontece se já houver algo no buffer. Em seguida, nossos dados serão divididos em 2 partes, uma preencherá o buffer até o final e os dados serão gravados no arquivo, o segundo será gravado no buffer se ele entrar nele ou diretamente no arquivo se não o fizer. Portanto, se criarmos um fluxo e gravar 4097 bytes nele, eles aparecerão imediatamente no arquivo, sem chamar Dispose. Se escrevermos 4095, nada estará no arquivo.
WindowsNo Windows, o algoritmo para usar o buffer e gravar diretamente é muito semelhante. Mas uma diferença significativa é observada diretamente nas chamadas de gravação e leitura do sistema assíncrono. Falando sem se aprofundar nas chamadas do sistema, existe uma estrutura sobreposta. Ele tem um campo importante para nós - HANDLE hEvent. Este é um evento de redefinição manual que entra em estado de alarme após a conclusão de uma operação. Voltar para a implementação. Gravando diretamente, assim como gravando no buffer, use chamadas assíncronas do sistema, que usam a estrutura acima como parâmetro. Ao gravar, um objeto FileStreamCompletionSource é criado - um herdeiro de TaskCompletionSource, no qual IOCallback é especificado. É chamado por encadeamento livre do pool quando a operação é concluída. No retorno de chamada, a estrutura Sobreposta é analisada e o objeto Tarefa é atualizado de acordo. Isso é tudo mágico.
Rede
É difícil descrever tudo o que vi entendendo a fonte. Meu caminho estava de HttpClient para Socket e SocketAsyncContext for Unix. O esquema geral é o mesmo que com os arquivos. Para Windows, a estrutura Sobreposta mencionada é usada e a operação é executada de forma assíncrona. No Unix, as operações de rede também usam funções de retorno de chamada.
E uma pequena explicação. Um leitor atento perceberá que, ao usar chamadas assíncronas entre uma chamada e um retorno de chamada, existe um certo vazio que, de alguma forma, funciona com os dados. Aqui vale a pena esclarecer a integridade. No exemplo de arquivos, o controlador de disco realiza operações diretas com o disco pelo controlador de disco, é ele quem dá os sinais sobre como mover as cabeças para o setor desejado, etc. O processador está livre no momento. A comunicação com o disco ocorre através das portas de entrada / saída. Eles indicam o tipo de operação, a localização dos dados no disco, etc. Em seguida, o controlador e o disco estão envolvidos nessa operação e, após a conclusão do trabalho, geram uma interrupção. Por conseguinte, uma chamada de sistema assíncrona apenas contribui com informações para as portas de entrada / saída, enquanto a chamada síncrona também espera pelos resultados, colocando o fluxo em um estado de bloqueio. Esse esquema não pretende ser absolutamente preciso (não sobre este artigo), mas fornece uma compreensão conceitual do trabalho.
Agora a natureza do processo está clara. Mas alguém pode perguntar, o que fazer com a assincronia? É impossível escrever de forma assíncrona sobre um método para sempre.
Primeiro de tudo. Um aplicativo pode ser feito como um serviço. Nesse caso, o ponto de entrada - Principal - é gravado do zero por você. Até recentemente, o Main não podia ser assíncrono; na versão 7 do idioma, esse recurso foi adicionado. Mas isso não muda nada radicalmente, apenas o compilador gera o Main comum e, a partir do assíncrono, apenas um método estático é feito, chamado Main e sua conclusão é esperada de forma síncrona. Então, provavelmente você tem algumas ações duradouras. Por alguma razão, neste momento, muitas pessoas começam a pensar em como criar threads para esse negócio: por meio de Task, ThreadPool ou Thread em geral manualmente, porque deve haver uma diferença em alguma coisa. A resposta é simples - é claro Tarefa. Se você usar a abordagem TAP, não interfira na criação manual de threads. Isso é semelhante ao uso do HttpClient para quase todas as solicitações, e o POST é feito de forma independente por meio do Socket.
Em segundo lugar. Aplicações web. Cada solicitação de entrada faz com que um novo thread seja extraído do ThreadPool para processamento. A piscina, é claro, é grande, mas não infinita. No caso de haver muitas solicitações, talvez não haja threads suficientes e todas as novas solicitações serão colocadas em fila para processamento. Essa situação é chamada de fome. Mas, no caso de usar controladores assíncronos, conforme discutido anteriormente, o fluxo retorna ao pool e pode ser usado para processar novas solicitações. Assim, a taxa de transferência do servidor é significativamente aumentada.
Observamos o processo assíncrono desde o início até o fim.
E armados com uma compreensão de toda essa assincronia, que contradiz a natureza humana, consideraremos alguns truques úteis ao trabalhar com código assíncrono.Classes e truques úteis ao trabalhar com a TAP
A diversidade estática da classe Task.
A classe Task possui vários métodos estáticos úteis. Abaixo estão os principais.- Task.WhenAny (..) - combinator, aceita IEnumerable / params de objetos de tarefa e retorna um objeto de tarefa que será concluído quando a primeira tarefa concluída. Ou seja, permite aguardar uma das várias tarefas em execução
- Task.WhenAll (..) - combinador, aceita IEnumerable / params de objetos de tarefa e retorna um objeto de tarefa, que será concluído após a conclusão de todas as tarefas transferidas
- Task.FromResult<T>(T value) — , .
- Task.Delay(..) —
- Task.Yield() — . , . , ,
ConfigureAwait
Naturalmente, o recurso "avançado" mais popular. Este método pertence à classe Task e permite especificar se precisamos continuar no mesmo contexto em que a operação assíncrona foi chamada. Por padrão, sem usar esse método, o contexto é lembrado e continuado nele, usando o método Post mencionado. No entanto, como dissemos, o Post é um prazer muito caro. Portanto, se o desempenho estiver em primeiro lugar e vermos que a continuação não atualizará, por exemplo, a interface do usuário, você poderá especificá-la no objeto de espera .ConfigureAwait (false) . Isso significa que não importa para nós onde a continuação será realizada.Agora sobre o problema. Como se costuma dizer, assustador não é ignorância, mas conhecimento falso.De alguma forma, observei o código de um aplicativo da Web, onde cada chamada assíncrona era decorada com esse acelerador. Isso não tem outro efeito senão nojo visual. O aplicativo Web padrão do ASP.NET Core não possui contextos exclusivos (a menos que você os escreva, é claro). Portanto, o método Post não é chamado lá de qualquer maneira.TaskCompletionSource <T>
Uma classe que facilita o gerenciamento de um objeto Tarefa. Uma classe tem amplas oportunidades, mas é mais útil quando queremos agrupar uma tarefa com uma ação, cujo final ocorre em um evento. Em geral, a classe foi criada para adaptar métodos assíncronos antigos à TAP, mas, como vimos, ela é usada não apenas para isso. Um pequeno exemplo de trabalho com esta classe:Exemplo public static Task<string> GetSomeDataAsync() { TaskCompletionSource<string> tcs = new TaskCompletionSource<string>(); FileSystemWatcher watcher = new FileSystemWatcher { Path = Directory.GetCurrentDirectory(), NotifyFilter = NotifyFilters.LastAccess, EnableRaisingEvents = true }; watcher.Changed += (o, e) => tcs.SetResult(e.FullPath); return tcs.Task; }
Essa classe cria um wrapper assíncrono para obter o nome do arquivo que foi acessado na pasta atual.CancellationTokenSource
Permite cancelar uma operação assíncrona. O esquema geral se assemelha ao uso de um TaskCompletionSource. Primeiro, é criado var cts = new CancellationTokenSource () , que, a propósito, é IDisposable, depois cts.Token é passado para operações assíncronas . Além disso, seguindo alguma lógica sua, sob certas condições, o método cts.Cancel () é chamado . Também pode se inscrever em um evento ou qualquer outra coisa.Usar um CancellationToken é uma boa prática. Ao escrever seu método assíncrono que funciona em segundo plano, digamos que em um tempo infinito, você pode simplesmente inserir uma linha no corpo do loop: cancelellationToken.ThrowIfCancellationRequested () , que gerará uma exceçãoOperationCanceledException . Essa exceção é tratada como um cancelamento da operação e não é salva como uma exceção dentro do objeto de tarefa. Além disso, a propriedade IsCanceled no objeto Task se tornará verdadeira.Longrunning
Muitas vezes, existem situações, especialmente ao escrever serviços, quando você cria várias tarefas que funcionam durante toda a vida útil do serviço ou apenas por um período muito longo. Como lembramos, o uso de um conjunto de encadeamentos é justamente a sobrecarga da criação de um encadeamento. No entanto, se um fluxo raramente for criado (mesmo uma vez por hora), esses custos serão nivelados e você poderá criar fluxos separados com segurança. Para fazer isso, ao criar uma tarefa, você pode especificar uma opção especial:Task.Factory.StartNew (action, TaskCreationOptions.LongRunning )De qualquer forma, recomendo que você analise toda a sobrecarga de task.Factory.StartNew , existem várias maneiras de configurar a tarefa de maneira flexível para atender a necessidades específicas.Exceções
Devido à natureza não determinística da execução assíncrona de código, a questão das exceções é muito relevante. Seria uma pena se você não pudesse capturar a exceção e ela foi lançada no segmento esquerdo, interrompendo o processo. Uma classe ExceptionDispatchInfo foi criada para capturar uma exceção em um thread e lançá-lo nele . Para capturar a exceção, é usado o método estático ExceptionDispatchInfo.Capture (ex), que retorna ExceptionDispatchInfo.Um link para esse objeto pode ser passado para qualquer thread, que chama o método Throw () para jogá-lo fora. O lançamento em si NÃO ocorre no local da chamada de operação assíncrona, mas no local de uso do operador aguardar. E como você sabe, aguardar não pode ser aplicado ao vazio. Assim, se o contexto existir, ele será passado pelo método Post. Caso contrário, ele ficará animado no fluxo da piscina. E isso é quase 100% olá ao colapso do aplicativo. E aqui chegamos à prática do fato de que deveríamos usar a Tarefa ou a Tarefa <T>, mas não nulas.E mais uma coisa. O planejador possui um evento TaskScheduler.UnobservedTaskException que é acionado quando uma UnobservedTaskException é lançada. Essa exceção é lançada durante a coleta de lixo quando o GC tenta coletar um objeto de tarefa que possui uma exceção não tratada.IAsyncEnumerable
Antes do C # 8 e do .NET Core 3.0, não era possível usar um iterador de rendimento em um método assíncrono, o que complicava a vida e fazia com que ele retornasse a tarefa <IEnumerable <T>> desse método, ou seja, não havia como iterar a coleção até que ela fosse totalmente recebida. Agora existe uma oportunidade. Saiba mais aqui . Para isso, o tipo de retorno deve ser IAsyncEnumerable <T> (ou IAsyncEnumerator <T> ). Para percorrer essa coleção, você deve usar o loop foreach com a palavra-chave wait. Além disso, os métodos WithCancellation e ConfigureAwait podem ser chamados no resultado da operação , indicando o CancelationToken usado e a necessidade de continuar no mesmo contexto.Como esperado, tudo é feito o mais preguiçosamente possível.Abaixo está um exemplo e a conclusão que ele dá.Exemplo public class Program { public static async Task Main() { Stopwatch sw = new Stopwatch(); sw.Start(); IAsyncEnumerable<int> enumerable = AsyncYielding(); Console.WriteLine($"Time after calling: {sw.ElapsedMilliseconds}"); await foreach (var element in enumerable.WithCancellation(..).ConfigureAwait(false)) { Console.WriteLine($"element: {element}"); Console.WriteLine($"Time: {sw.ElapsedMilliseconds}"); } } static async IAsyncEnumerable<int> AsyncYielding() { foreach (var uselessElement in Enumerable.Range(1, 3)) { Task task = Task.Delay(TimeSpan.FromSeconds(uselessElement)); Console.WriteLine($"Task run: {uselessElement}"); await task; yield return uselessElement; } } }
Conclusão:Tempo após a chamada: 0
Execução da tarefa: 1
elemento: 1
Tempo: 1033 Execução da
tarefa: 2
elemento: 2
Tempo: 3034 Execução da
tarefa: 3
elemento: 3
Tempo: 6035Threadpool
Esta classe é usada ativamente ao programar com TAP. Portanto, darei os detalhes mínimos de sua implementação. Por dentro, o ThreadPool tem uma matriz de filas: uma para cada thread + uma global. Ao adicionar um novo trabalho ao pool, o encadeamento que iniciou a adição é levado em consideração. Se for um encadeamento do pool, o trabalho será colocado em sua própria fila para esse encadeamento, se houver outro encadeamento - no global. Quando um segmento é selecionado para funcionar, sua fila local é exibida pela primeira vez. Se estiver vazio, o encadeamento recebe trabalhos do global. Se estiver vazio, começa a roubar dos outros. Além disso, você nunca deve confiar na ordem do trabalho, porque, de fato, não há ordem. O número padrão de threads em um pool depende de muitos fatores, incluindo o tamanho do espaço de endereço. Se houver mais pedidos de execução,além do número de threads disponíveis, os pedidos são enfileirados.Os encadeamentos em um conjunto de encadeamentos são encadeamentos em segundo plano (propriedade isBackground = true). Esse tipo de encadeamento não suporta a vida útil do processo se todos os encadeamentos em primeiro plano foram concluídos.O encadeamento do sistema monitora o status da alça de espera. Quando a operação de espera termina, o retorno de chamada transferido é executado pelo encadeamento do pool (lembre-se dos arquivos no Windows).Tipo de tarefa
Mencionado anteriormente, esse tipo (estrutura ou classe) pode ser usado como o valor de retorno do método assíncrono. Um tipo de construtor deve ser associado a esse tipo usando o atributo [AsyncMethodBuilder (..)] . Esse tipo deve ter as características mencionadas acima para poder aplicar a palavra-chave wait. Pode ser parametrizado para métodos que não retornam um valor e parametrizado para aqueles que retornam.O próprio construtor é uma classe ou estrutura cuja estrutura é mostrada no exemplo abaixo. O método SetResult possui um parâmetro do tipo T para um tipo de tarefa parametrizado por T. Para tipos não parametrizados, o método não possui parâmetros.Interface do Construtor Necessária class MyTaskMethodBuilder<T> { public static MyTaskMethodBuilder<T> Create(); public void Start<TStateMachine>(ref TStateMachine stateMachine) where TStateMachine : IAsyncStateMachine; public void SetStateMachine(IAsyncStateMachine stateMachine); public void SetException(Exception exception); public void SetResult(T result); public void AwaitOnCompleted<TAwaiter, TStateMachine>( ref TAwaiter awaiter, ref TStateMachine stateMachine) where TAwaiter : INotifyCompletion where TStateMachine : IAsyncStateMachine; public void AwaitUnsafeOnCompleted<TAwaiter, TStateMachine>( ref TAwaiter awaiter, ref TStateMachine stateMachine) where TAwaiter : ICriticalNotifyCompletion where TStateMachine : IAsyncStateMachine; public MyTask<T> Task { get; } }
O princípio do trabalho do ponto de vista da escrita do seu tipo Tarefa será descrito abaixo. A maior parte disso já foi descrita ao analisar o código gerado pelo compilador.O compilador usa todos esses tipos para gerar uma máquina de estado. O compilador sabe quais construtores usar para os tipos que conhece, aqui especificamos o que será usado durante a geração do código. Se a máquina de estado for uma estrutura, ela será compactada ao chamar SetStateMachine , o construtor poderá armazenar em cache a cópia compactada, se necessário. O construtor deve chamar stateMachine.MoveNext no método Start ou depois de ser chamado para iniciar a execução e avançar a máquina de estado. Após chamar Start, a propriedade Task será retornada do método Eu recomendo que você retorne ao método stub e veja essas etapas.Se a máquina de estados for concluída com êxito, o método SetResult será chamado , caso contrário, SetException . Se a máquina de estado chegar em espera, o método GetAwaiter () do tipo tarefa é executado . Se o objeto wait implementar a interface ICriticalNotifyCompletion e IsCompleted = false, a máquina de estado utilizará o builder.AwaitUnsafeOnCompleted (ref wait, ref stateMachine) . O método AwaitUnsafeOnCompleted deve chamar waititer.OnCompleted (action) , a ação deve chamar stateMachine.MoveNextquando o objeto de espera for concluído. Da mesma forma, para a interface INotifyCompletion e o método builder.AwaitOnCompleted .Como usar isso é com você. Mas eu aconselho você a pensar 514 vezes antes de aplicar isso na produção, e não para mimar. A seguir, é apresentado um exemplo de uso. Esbocei apenas um proxy para um construtor padrão que exibe no console qual método foi chamado e a que horas. A propósito, o Main () assíncrono não deseja oferecer suporte a um tipo de espera personalizado (acredito que mais de um projeto de produção foi irremediavelmente corrompido devido a essa falha da Microsoft). Se desejar, você pode modificar o logger proxy usando um logger normal e registrando mais informações.Tarefa de proxy de log public class Program { public static void Main() { Console.WriteLine("Start"); JustMethod().Task.Wait();
Conclusão:Iniciar
método: Criar; 2019-10-09T17: 55: 13.7152733 + 03: 00
Método: Iniciar; 2019-10-09T17: 55: 13.7262226 + 03: 00
Método: AwaitUnsafeOnCompleted; 2019-10-09T17: 55: 13.7275206 + 03: 00
propriedade: tarefa; 2019-10-09T17: 55: 13.7292005 + 03: 00
Método: SetResult; 2019-10-09T17: 55: 14.7297967 + 03: 00
StopÉ tudo, obrigado a todos.