Vez após vez, vejo que o uso de async
- await
não cria nenhum thread adicional. Isso não faz sentido, porque as únicas maneiras pelas quais um computador pode parecer estar fazendo mais de uma coisa por vez é
- Realmente fazendo mais de uma coisa de cada vez (executando em paralelo, usando vários processadores)
- Simulando-o agendando tarefas e alternando entre elas (faça um pouco de A, um pouco de B, um pouco de A etc.)
Então, se async
- await
não faz nenhum desses, então como ele pode tornar um aplicativo responsivo? Se houver apenas 1 thread, chamar qualquer método significa aguardar a conclusão do método antes de fazer qualquer outra coisa, e os métodos contidos nesse método deverão aguardar o resultado antes de prosseguir, e assim por diante.
c#
.net
multithreading
asynchronous
async-await
Senhora Corlib
fonte
fonte
await
/async
funciona sem criar nenhum thread.Respostas:
Na verdade, assíncrono / espera não é tão mágico. O tópico completo é bastante amplo, mas acho que podemos responder a uma pergunta rápida e completa o suficiente para sua pergunta.
Vamos abordar um simples evento de clique no botão em um aplicativo Windows Forms:
Eu explicitamente não vou falar sobre o que
GetSomethingAsync
está retornando por enquanto. Digamos que isso é algo que será concluído após, digamos, 2 segundos.Em um mundo tradicional não assíncrono, o manipulador de eventos de clique no botão seria algo parecido com isto:
Quando você clica no botão no formulário, o aplicativo parece congelar por cerca de 2 segundos, enquanto aguardamos a conclusão desse método. O que acontece é que a "bomba de mensagens", basicamente um loop, está bloqueada.
Esse loop pergunta continuamente no Windows "Alguém fez alguma coisa, como mover o mouse, clicou em alguma coisa? Preciso repintar alguma coisa? Se sim, me diga!" e depois processa esse "algo". Esse loop recebeu uma mensagem de que o usuário clicou em "button1" (ou o tipo equivalente de mensagem do Windows) e acabou chamando nosso
button1_Click
método acima. Até que esse método retorne, esse loop ficará parado aguardando. Isso leva 2 segundos e, durante isso, nenhuma mensagem está sendo processada.A maioria das coisas que lidam com janelas são feitas usando mensagens, o que significa que, se o loop de mensagens parar de bombear mensagens, mesmo por apenas um segundo, ele será rapidamente percebido pelo usuário. Por exemplo, se você mover o bloco de notas ou qualquer outro programa em cima do seu próprio programa e depois se afastar novamente, uma enxurrada de mensagens de tinta será enviada ao seu programa, indicando qual região da janela que agora se tornou visível novamente. Se o loop de mensagens que processa essas mensagens estiver aguardando algo bloqueado, nenhuma pintura será feita.
Portanto, se no primeiro exemplo,
async/await
não cria novos threads, como isso acontece?Bem, o que acontece é que seu método é dividido em dois. Este é um desses tipos de tópicos abrangentes, por isso não entrarei em muitos detalhes, mas basta dizer que o método está dividido nessas duas coisas:
await
, incluindo a chamada paraGetSomethingAsync
await
Ilustração:
Reorganizados:
Basicamente, o método é executado assim:
await
Ele chama o
GetSomethingAsync
método, que faz sua parte, e retorna algo que será concluído em 2 segundos no futuroAté agora, ainda estamos dentro da chamada original para button1_Click, acontecendo no thread principal, chamado a partir do loop de mensagens. Se o código anterior
await
levar muito tempo, a interface do usuário continuará congelada. No nosso exemplo, nem tantoO que a
await
palavra - chave, juntamente com alguma mágica inteligente do compilador, faz é basicamente algo como "Ok, quer saber, eu simplesmente retornarei do manipulador de eventos do clique no botão aqui. Quando você (como em, a coisa que nós ' estou aguardando) chegar ao fim, avise-me porque ainda tenho algum código para executar ".Na verdade, ele permitirá que a classe SynchronizationContext saiba que está pronto, o que, dependendo do contexto de sincronização real em execução no momento, ficará na fila para execução. A classe de contexto usada em um programa Windows Forms a enfileirará usando a fila que o loop da mensagem está bombeando.
Então, ele volta ao loop de mensagens, que agora está livre para continuar enviando mensagens, como mover a janela, redimensioná-la ou clicar em outros botões.
Para o usuário, a interface do usuário agora é responsiva novamente, processando outros cliques em botões, redimensionando e, o mais importante, redesenhando , para que não pare de congelar.
await
e continua executando o restante do método. Observe que esse código é chamado novamente a partir do loop de mensagens; portanto, se esse código fizer algo demorado sem usarasync/await
corretamente, ele bloqueará novamente o loop de mensagensExistem muitas partes móveis por baixo do capô aqui, então aqui estão alguns links para mais informações, eu diria "se precisar", mas esse tópico é bastante amplo e é bastante importante conhecer algumas dessas partes móveis . Invariavelmente, você entenderá que assíncrono / espera ainda é um conceito que vaza. Algumas das limitações e problemas subjacentes ainda vazam para o código circundante e, se não o fizerem, você geralmente acaba depurando um aplicativo que quebra aleatoriamente sem, aparentemente, um bom motivo.
OK, e daí se
GetSomethingAsync
gerar um thread que será concluído em 2 segundos? Sim, obviamente existe uma nova discussão em jogo. Esse encadeamento, no entanto, não é devido à assíncrona deste método, é porque o programador deste método escolheu um encadeamento para implementar código assíncrono. Quase todas as E / S assíncronas não usam um encadeamento, elas usam coisas diferentes.async/await
por si só, não ativam novos threads, mas obviamente as "coisas pelas quais esperamos" podem ser implementadas usando threads.Há muitas coisas no .NET que não necessariamente geram um thread por conta própria, mas ainda são assíncronas:
SomethingSomethingAsync
ouBeginSomething
eEndSomething
e há umaIAsyncResult
envolvidos.Geralmente essas coisas não usam um fio debaixo do capô.
OK, então você quer algumas dessas "coisas abrangentes"?
Bem, vamos perguntar ao Try Roslyn sobre o nosso clique no botão:
Experimente Roslyn
Não vou vincular a classe gerada aqui, mas é uma coisa bem sangrenta.
fonte
await
pode ser usado da maneira que você descreve, mas geralmente não é. Somente os retornos de chamada são agendados (no pool de threads) - entre o retorno de chamada e a solicitação, nenhum thread é necessário.Eu explico na íntegra no meu blog Não há discussão .
Em resumo, os modernos sistemas de E / S fazem uso pesado de DMA (Direct Memory Access). Existem processadores especiais dedicados em placas de rede, placas de vídeo, controladores de disco rígido, portas seriais / paralelas, etc. Esses processadores têm acesso direto ao barramento de memória e lidam com a leitura / gravação de maneira totalmente independente da CPU. A CPU apenas precisa notificar o dispositivo sobre o local na memória que contém os dados e, em seguida, pode fazer suas próprias ações até que o dispositivo levante uma interrupção notificando a CPU que a leitura / gravação está concluída.
Uma vez que a operação está em andamento, não há trabalho para a CPU e, portanto, nenhuma thread.
fonte
Task.Run
é o mais apropriado para ações vinculadas à CPU , mas também tem vários outros usos.Não é que esperar, nenhum desses. Lembre-se, o objetivo de
await
não é tornar código síncrono magicamente assíncrono . É para habilitar o uso das mesmas técnicas que usamos para escrever código síncrono ao chamar código assíncrono . Aguardar é tornar o código que usa operações de alta latência se parece com o código que usa operações de baixa latência . Essas operações de alta latência podem estar em threads, podem estar em hardware de propósito específico, podem estar dividindo seu trabalho em pequenos pedaços e colocando-o na fila de mensagens para processamento posterior no thread da interface do usuário. Eles estão fazendo algo para conseguir assincronia, mas elessão os que estão fazendo isso. Aguardar permite que você tire proveito dessa assincronia.Além disso, acho que está faltando uma terceira opção. Nós, idosos - as crianças de hoje com seu rap devem sair do meu gramado etc. - lembramos do mundo do Windows no início dos anos 90. Não havia máquinas com várias CPUs nem agendadores de threads. Você queria executar dois aplicativos do Windows ao mesmo tempo, teve que ceder . A multitarefa foi cooperativa . O sistema operacional informa ao processo que ele deve ser executado e, se for mal comportado, impede que todos os outros processos sejam atendidos. Ele funciona até render e, de alguma forma, precisa saber de onde parou na próxima vez em que o sistema operacional voltar a controlar. O código assíncrono de thread único é muito parecido com isso, com "aguardar" em vez de "render". Aguardar significa "Vou me lembrar de onde parei aqui e deixar outra pessoa correr por um tempo; me ligue de volta quando a tarefa que eu estiver esperando estiver concluída e eu continuarei de onde parei". Acho que você pode ver como isso torna os aplicativos mais responsivos, assim como nos 3 dias do Windows.
Existe a chave que está faltando. Um método pode retornar antes que seu trabalho seja concluído . Essa é a essência da assincronia ali. Um método retorna, retorna uma tarefa que significa "este trabalho está em andamento; diga-me o que fazer quando estiver concluído". O trabalho do método não está concluído, mesmo que tenha retornado .
Antes do operador de espera, era necessário escrever um código que parecia espaguete enfiado no queijo suíço para lidar com o fato de que temos trabalho a fazer após a conclusão, mas com o retorno e a conclusão dessincronizados . Aguardar permite que você escreva um código que se pareça com o retorno e a conclusão são sincronizados, sem que eles realmente sejam sincronizados.
fonte
yield
palavra - chave Osasync
métodos e os iteradores em C # são uma forma de corotina , que é o termo geral para uma função que sabe como suspender sua operação atual para reiniciar posteriormente. Atualmente, vários idiomas têm corotinas ou fluxos de controle semelhantes a corotina.Estou realmente feliz que alguém tenha feito essa pergunta, porque durante muito tempo também acreditei que os threads eram necessários para simultaneidade. Quando vi pela primeira vez loops de eventos , pensei que eram uma mentira. Pensei comigo mesmo "não há como esse código ser simultâneo se for executado em um único thread". Lembre-se de que isso aconteceu depois que eu já havia passado pela luta de entender a diferença entre simultaneidade e paralelismo.
Após a pesquisa do meu próprio, eu finalmente encontrei a peça que faltava:
select()
. Especificamente, IO multiplexação, implementado por vários kernels sob diferentes nomes:select()
,poll()
,epoll()
,kqueue()
. São chamadas de sistema que, embora os detalhes da implementação sejam diferentes, permitem que você transmita um conjunto de descritores de arquivos para observação. Em seguida, você pode fazer outra chamada que bloqueia até que um dos descritores de arquivo monitorados seja alterado.Assim, pode-se esperar um conjunto de eventos de E / S (o loop de eventos principal), manipular o primeiro evento que é concluído e, em seguida, retornar o controle ao loop de eventos. Enxague e repita.
Como é que isso funciona? Bem, a resposta curta é que é mágica em nível de kernel e hardware. Existem muitos componentes em um computador além da CPU, e esses componentes podem funcionar em paralelo. O kernel pode controlar esses dispositivos e se comunicar diretamente com eles para receber certos sinais.
Essas chamadas de sistema de multiplexação de E / S são o alicerce fundamental de loops de eventos de thread único como node.js ou Tornado. Quando você
await
executa uma função, está observando um determinado evento (conclusão dessa função) e, em seguida, retorna o controle para o loop do evento principal. Quando o evento que você está assistindo termina, a função (eventualmente) é retomada de onde parou. Funções que permitem suspender e retomar a computação como essa são chamadas corotinas .fonte
await
easync
use Tarefas, não Threads.A estrutura possui um pool de threads prontos para executar algum trabalho na forma de objetos Task ; enviar uma tarefa ao pool significa selecionar um thread 1 já existente e gratuito para chamar o método de ação da tarefa. Criar uma tarefa é criar um novo objeto, muito mais rápido do que criar um novo encadeamento.
Dado que uma tarefa é possível anexar uma continuação a ela, é um novo objeto de tarefa a ser executado assim que o encadeamento terminar.
Como
async/await
usam as Tarefas, eles não criam um novo thread.Embora a técnica de programação de interrupção seja amplamente usada em todos os sistemas operacionais modernos, não acho que sejam relevantes aqui.
Você pode ter duas tarefas ligadas à CPU executando em paralelo (intercaladas na verdade) em uma única CPU usando
aysnc/await
.Isso não pôde ser explicado simplesmente pelo fato de o sistema operacional suportar IORP na fila .
Na última vez em que verifiquei os
async
métodos transformados pelo compilador no DFA , o trabalho foi dividido em etapas, cada uma terminando com umaawait
instrução.O
await
inicia sua tarefa e anexa uma continuação para executar a próxima etapa.Como exemplo de conceito, aqui está um exemplo de pseudo-código.
As coisas estão sendo simplificadas por uma questão de clareza e porque não me lembro de todos os detalhes exatamente.
É transformado em algo assim
1 Na verdade, um pool pode ter sua política de criação de tarefas.
fonte
Não vou competir com Eric Lippert ou Lasse V. Karlsen e outros, apenas gostaria de chamar a atenção para outra faceta desta questão, que acho que não foi mencionada explicitamente.
Usar
await
sozinho não torna seu aplicativo responsivo magicamente. Se o que você fizer no método que você está aguardando nos blocos de encadeamento da interface do usuário, ele ainda bloqueará sua interface do usuário da mesma forma que a versão não aguardável .Você precisa escrever seu método aguardável especificamente para gerar um novo encadeamento ou usar algo como uma porta de conclusão (que retornará a execução no encadeamento atual e chamará outra coisa para continuação sempre que a porta de conclusão for sinalizada). Mas esta parte está bem explicada em outras respostas.
fonte
Aqui está como eu vejo tudo isso, pode não ser super tecnicamente preciso, mas me ajuda a pelo menos :).
Existem basicamente dois tipos de processamento (computação) que ocorrem em uma máquina:
Portanto, quando escrevemos um código-fonte, após a compilação, dependendo do objeto que usamos (e isso é muito importante), o processamento será vinculado à CPU ou IO e, de fato, pode ser vinculado a uma combinação de ambos.
Alguns exemplos:
FileStream
objeto (que é um Stream), o processamento será, 1% de CPU associado e 99% de IO.NetworkStream
objeto (que é um Stream), o processamento será, 1% de CPU associado e 99% de IO.Memorystream
objeto (que é um Stream), o processamento será 100% vinculado à CPU.Então, como você vê, do ponto de vista de um programador orientado a objetos, embora eu esteja sempre acessando um
Stream
objeto, o que acontece abaixo pode depender muito do tipo final do objeto.Agora, para otimizar as coisas, às vezes é útil poder executar o código em paralelo (observe que eu não uso a palavra assíncrona) se for possível e / ou necessário.
Alguns exemplos:
Antes de assíncrono / espera, tínhamos basicamente duas soluções para isso:
O assíncrono / espera é apenas um modelo de programação comum, baseado no conceito de Tarefa . É um pouco mais fácil de usar do que threads ou conjuntos de threads para tarefas vinculadas à CPU e muito mais fácil de usar que o antigo modelo Begin / End. Disfarçado, no entanto, é "apenas" um invólucro super sofisticado e cheio de recursos em ambos.
Portanto, a verdadeira vitória está principalmente nas tarefas IO Bound , tarefa que não usa a CPU, mas async / waitit ainda é apenas um modelo de programação, não ajuda a determinar como / onde o processamento acontecerá no final.
Isso significa que não é porque uma classe tem um método "DoSomethingAsync" retornando um objeto Task que você pode presumir que ele esteja vinculado à CPU (o que significa que pode ser bastante inútil , especialmente se não tiver um parâmetro de token de cancelamento) ou IO Bound (o que significa que provavelmente é uma obrigação ) ou uma combinação de ambos (como o modelo é bastante viral, os vínculos e os possíveis benefícios podem ser, no final, super misturados e não tão óbvios).
Então, voltando aos meus exemplos, realizar minhas operações de gravação usando async / waitit no MemoryStream permanecerá vinculado à CPU (provavelmente não me beneficio disso), embora certamente me beneficie com arquivos e fluxos de rede.
fonte
Resumindo outras respostas:
O assíncrono / espera é criado principalmente para tarefas vinculadas de E / S, pois, ao usá-las, é possível evitar o bloqueio do encadeamento de chamada. Seu uso principal é com threads da interface do usuário, onde não é desejado que o thread seja bloqueado em uma operação vinculada à E / S.
O Async não cria seu próprio encadeamento. O encadeamento do método de chamada é usado para executar o método assíncrono até encontrar um esperado. O mesmo encadeamento continua a executar o restante do método de chamada além da chamada de método assíncrona. Dentro do método chamado assíncrono, após retornar do esperado, a continuação pode ser executada em um encadeamento do pool de encadeamentos - o único local em que um encadeamento separado é exibido.
fonte
Eu tento explicar de baixo para cima. Talvez alguém ache útil. Eu estava lá, fiz isso, reinventei, quando fiz jogos simples no DOS em Pascal (bons velhos tempos ...)
Então ... Em um aplicativo orientado a todos os eventos, há um loop de eventos dentro, algo assim:
As estruturas geralmente escondem esses detalhes de você, mas estão lá. A função getMessage lê o próximo evento da fila de eventos ou aguarda até que um evento aconteça: movimentação do mouse, tecla pressionada, keyup, clique etc. E, em seguida, dispatchMessage envia o evento para o manipulador de eventos apropriado. Em seguida, aguarda o próximo evento e assim por diante até que um evento de encerramento saia dos loops e finalize o aplicativo.
Os manipuladores de eventos devem ser executados rapidamente para que o loop de eventos possa pesquisar mais eventos e a interface do usuário permaneça responsiva. O que acontece se um clique no botão acionar uma operação cara como essa?
Bem, a interface do usuário fica sem resposta até que a operação de 10 segundos seja concluída enquanto o controle permanece dentro da função. Para resolver esse problema, você precisa dividir a tarefa em pequenas partes que podem ser executadas rapidamente. Isso significa que você não pode lidar com a coisa toda em um único evento. Você deve fazer uma pequena parte do trabalho e depois postar outro evento na fila de eventos para solicitar continuação.
Então você mudaria isso para:
Nesse caso, apenas a primeira iteração é executada e, em seguida, ela envia uma mensagem para a fila de eventos para executar a próxima iteração e retorna. É nosso exemplo de
postFunctionCallMessage
pseudo-função que coloca um evento "call this function" na fila, para que o distribuidor de eventos o chame quando o alcançar. Isso permite que todos os outros eventos da GUI sejam processados ao executar continuamente partes de um trabalho de longa execução também.Enquanto essa tarefa de longa execução estiver em execução, seu evento de continuação estará sempre na fila de eventos. Então você basicamente inventou seu próprio agendador de tarefas. Onde os eventos de continuação na fila são "processos" em execução. Na verdade, é isso que os sistemas operacionais fazem, exceto que o envio dos eventos de continuação e o retorno ao loop do agendador são feitos através da interrupção do temporizador da CPU, onde o SO registrou o código de mudança de contexto, para que você não precise se preocupar com isso. Mas aqui você está escrevendo seu próprio agendador e precisa se preocupar com isso - até agora.
Portanto, podemos executar tarefas de execução longa em um único encadeamento paralelo à GUI, dividindo-as em pequenos blocos e enviando eventos de continuação. Esta é a ideia geral da
Task
classe. Ele representa uma peça de trabalho e, quando você a chama.ContinueWith
, define qual função chamar como a próxima peça quando a peça atual terminar (e seu valor de retorno é passado para a continuação). ATask
classe usa um pool de encadeamentos, em que há um loop de eventos em cada encadeamento aguardando para executar trabalhos semelhantes ao que eu mostrei no início. Dessa forma, você pode ter milhões de tarefas em execução em paralelo, mas apenas alguns threads para executá-las. Mas funcionaria tão bem com um único thread - desde que suas tarefas sejam adequadamente divididas em pequenos pedaços, cada um deles aparecendo em paralelo.Mas fazer todo esse encadeamento, dividindo o trabalho em pequenos pedaços manualmente, é um trabalho complicado e atrapalha totalmente o layout da lógica, porque todo o código da tarefa em segundo plano basicamente é uma
.ContinueWith
bagunça. Então é aqui que o compilador ajuda você. Faz todo esse encadeamento e continuação para você em segundo plano. Quando você dizawait
que diz ao compilador que "pare aqui, adicione o restante da função como uma tarefa de continuação". O compilador cuida do resto, para que você não precise.fonte
Na verdade,
async await
cadeias são máquinas de estado geradas pelo compilador CLR.async await
no entanto, usa threads que o TPL está usando pool de threads para executar tarefas.O motivo pelo qual o aplicativo não está bloqueado é que a máquina de estado pode decidir qual co-rotina executar, repetir, verificar e decidir novamente.
Leitura adicional:
O que async & waitit gera?
Espera assíncrona e o StateMachine gerado
C # assíncrono e F # (III.): Como funciona? - Tomas Petricek
Editar :
OK. Parece que minha elaboração está incorreta. No entanto, tenho de salientar que as máquinas de estado são ativos importantes para
async await
s. Mesmo se você receber E / S assíncrona, ainda precisará de um ajudante para verificar se a operação está concluída; portanto, ainda precisamos de uma máquina de estado e determinar qual rotina pode ser executada de maneira assícrona.fonte
Isso não está respondendo diretamente à pergunta, mas acho que é uma informação adicional interessante:
Assíncrono e aguardar não cria novos threads por si só. MAS, dependendo de onde você usa a espera assíncrona, a parte síncrona ANTES da espera pode ser executada em um thread diferente da parte síncrona APÓS a espera (por exemplo, o núcleo do ASP.NET e ASP.NET se comporta de maneira diferente).
Nos aplicativos baseados em thread da interface do usuário (WinForms, WPF), você estará no mesmo thread antes e depois. Mas quando você usa async away em um thread do pool de Threads, o thread antes e depois da espera pode não ser o mesmo.
Um ótimo vídeo sobre este tópico
fonte