Se o async-waitit não cria nenhum thread adicional, como ele torna os aplicativos responsivos?

242

Vez após vez, vejo que o uso de async- awaitnã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- awaitnã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.

Senhora Corlib
fonte
17
As tarefas de E / S não são vinculadas à CPU e, portanto, não requerem um encadeamento. O ponto principal do assíncrono é não bloquear threads durante tarefas associadas a E / S.
21416 juharr
24
@jdweng: Não, de jeito nenhum. Mesmo que tenha criado novos threads , isso é muito diferente de criar um novo processo.
Jon Skeet
8
Se você entende a programação assíncrona baseada em retorno de chamada, entende como await/ asyncfunciona sem criar nenhum thread.
User253751
6
Ele não torna exatamente um aplicativo mais responsivo, mas desencoraja você a bloquear seus threads, o que é uma causa comum de aplicativos que não respondem.
Owen
6
@RubberDuck: Sim, ele pode usar um thread do pool de threads para a continuação. Mas não está iniciando um encadeamento da maneira que o OP imagina aqui - não é como se ele dissesse "Pegue este método comum, agora execute-o em um encadeamento separado - aí, isso é assíncrono". É muito mais sutil do que isso.
Jon Skeet

Respostas:

299

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:

public async void button1_Click(object sender, EventArgs e)
{
    Console.WriteLine("before awaiting");
    await GetSomethingAsync();
    Console.WriteLine("after awaiting");
}

Eu explicitamente não vou falar sobre o que GetSomethingAsyncestá 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:

public void button1_Click(object sender, EventArgs e)
{
    Console.WriteLine("before waiting");
    DoSomethingThatTakes2Seconds();
    Console.WriteLine("after waiting");
}

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_Clickmé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/awaitnã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:

  1. Todo o código que antecede await, incluindo a chamada paraGetSomethingAsync
  2. Todo o código a seguir await

Ilustração:

code... code... code... await X(); ... code... code... code...

Reorganizados:

code... code... code... var x = X(); await X; code... code... code...
^                                  ^          ^                     ^
+---- portion 1 -------------------+          +---- portion 2 ------+

Basicamente, o método é executado assim:

  1. Ele executa tudo até await
  2. Ele chama o GetSomethingAsyncmétodo, que faz sua parte, e retorna algo que será concluído em 2 segundos no futuro

    Até 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 awaitlevar muito tempo, a interface do usuário continuará congelada. No nosso exemplo, nem tanto

  3. O que a awaitpalavra - 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.

  4. 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.

  5. Dois segundos depois, o que estamos aguardando é concluído e o que acontece agora é que ele (bem, o contexto de sincronização) coloca uma mensagem na fila que o loop de mensagens está olhando, dizendo "Ei, eu tenho mais código para você executar ", e este código é todo o código após a espera.
  6. Quando o loop da mensagem chega a essa mensagem, ele basicamente "reinsere" o método de onde parou, logo após awaite 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 usar async/awaitcorretamente, ele bloqueará novamente o loop de mensagens

Existem 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 GetSomethingAsyncgerar 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:

  • Solicitações da Web (e muitas outras coisas relacionadas à rede que levam tempo)
  • Leitura e gravação assíncrona de arquivos
  • e muitos mais, um bom sinal é se a classe / Interface em questão métodos chamado SomethingSomethingAsyncou BeginSomethinge EndSomethinge há uma IAsyncResultenvolvidos.

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.

Lasse V. Karlsen
fonte
11
Portanto, é basicamente o que o OP descreveu como " Simulando a execução paralela agendando tarefas e alternando entre elas ", não é?
Bergi 24/05
4
@ Bergi Não é bem assim. A execução é realmente paralela - a tarefa de E / S assíncrona está em andamento e não requer threads para prosseguir (isso é algo que foi usado muito antes do Windows aparecer) - O MS DOS também usou E / S assíncrona, mesmo que não o fizesse. tem multi-threading!). Obviamente, também 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.
Luaan 25/05
3
É por isso que eu queria evitar explicitamente falar muito sobre o que esse método fez, pois a pergunta era sobre assíncrono / espera especificamente, o que não cria seus próprios threads. Obviamente, eles podem ser usados ​​para aguardar a conclusão dos threads.
Lasse V. Karlsen
6
@ LasseV.Karlsen - Estou ingerindo sua ótima resposta, mas ainda estou desligando um detalhe. Entendo que o manipulador de eventos existe, como na etapa 4, que permite que a bomba de mensagens continue bombeando, mas quando e onde a "coisa que leva dois segundos" continua sendo executada, se não em um thread separado? Se fosse para executar no segmento interface do usuário, então seria bloquear a bomba de mensagem de qualquer maneira durante sua execução, porque ele tem para executar algum tempo no mesmo segmento .. [continuou] ...
rory.ap
3
Gosto da sua explicação com a bomba de mensagens. Como a sua explicação difere quando não há uma bomba de mensagens como no aplicativo de console ou servidor da web? Como a reentrada de um método é alcançada?
Puchacz
95

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.

Stephen Cleary
fonte
Só para esclarecer. Entendo o alto nível do que acontece ao usar o Async-Waitit. Com relação à criação sem encadeamento - não há encadeamento apenas nas solicitações de E / S para dispositivos que, como você disse, têm seus próprios processadores que tratam da própria solicitação? Podemos assumir que TODAS as solicitações de E / S são tratadas em processadores independentes, ou seja, use Task.Run ONLY em ações vinculadas à CPU?
Yonatan Nir
@YonatanNir: Não se trata apenas de processadores separados; qualquer tipo de resposta orientada a eventos é naturalmente assíncrono. Task.Runé o mais apropriado para ações vinculadas à CPU , mas também tem vários outros usos.
Stephen Cleary
1
Eu terminei de ler o seu artigo e ainda há algo básico que não entendo, pois não estou familiarizado com a implementação de nível mais baixo do sistema operacional. Eu recebi o que você escreveu até onde você escreveu: "A operação de gravação está agora" em andamento ". Quantos threads estão processando-a? Nenhuma." . Portanto, se não houver threads, como a operação será realizada se não estiver em um thread?
Yonatan Nir
6
Esta é a peça que faltava em milhares de explicações !!! Na verdade, há alguém fazendo o trabalho em segundo plano com operações de E / S. Não é um tópico, mas outro componente de hardware dedicado fazendo seu trabalho!
the_dark_destructor
2
@PrabuWeerasinghe: O compilador cria uma estrutura que mantém as variáveis ​​estaduais e locais. Se um aguardar precisar render (ou seja, retornar ao chamador), essa estrutura será colocada em caixa e permanecerá na pilha.
Stephen Cleary
87

as únicas maneiras pelas quais um computador parece estar fazendo mais de uma coisa de cada vez é: (1) Realizando mais de uma coisa de cada vez, (2) simulando-o agendando tarefas e alternando entre elas. Portanto, se assíncrono-aguardar, nenhum desses

Não é que esperar, nenhum desses. Lembre-se, o objetivo de awaitnã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.

chamar qualquer método significa aguardar a conclusão do método

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.

Eric Lippert
fonte
Outras linguagens modernas de alto nível também suportam comportamento explicitamente cooperativo semelhante (ou seja, a função faz algumas coisas, produz [possivelmente enviando algum valor / objeto ao chamador]) continua onde parou quando o controle é devolvido [possivelmente com entrada adicional fornecida] ) Os geradores são muito grandes em Python, por um lado.
JAB
2
@JAB: Absolutamente. Os geradores são chamados de "blocos iteradores" em C # e usam a yieldpalavra - chave Os asyncmé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.
Eric Lippert
1
A analogia a produzir é boa - é multitarefa cooperativa dentro de um processo. (e evitando desse modo os problemas de estabilidade do sistema de todo o sistema multitarefa cooperativa)
user253751
3
Eu acho que o conceito de "interrupções da CPU" sendo usado para E / S não é conhecido sobre muitos "programadores" modernos, portanto eles acham que um thread precisa esperar por cada E / S.
Ian Ringrose
Método @EricLippert Async de WebClient realmente cria segmento adicional, veja aqui stackoverflow.com/questions/48366871/...
KevinBui
28

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ê awaitexecuta 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 .

Gardenhead
fonte
25

awaite asyncuse 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/awaitusam 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 asyncmétodos transformados pelo compilador no DFA , o trabalho foi dividido em etapas, cada uma terminando com uma awaitinstrução.
O awaitinicia 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.

method:
   instr1                  
   instr2
   await task1
   instr3
   instr4
   await task2
   instr5
   return value

É transformado em algo assim

int state = 0;

Task nextStep()
{
  switch (state)
  {
     case 0:
        instr1;
        instr2;
        state = 1;

        task1.addContinuation(nextStep());
        task1.start();

        return task1;

     case 1:
        instr3;
        instr4;
        state = 2;

        task2.addContinuation(nextStep());
        task2.start();

        return task2;

     case 2:
        instr5;
        state = 0;

        task3 = new Task();
        task3.setResult(value);
        task3.setCompleted();

        return task3;
   }
}

method:
   nextStep();

1 Na verdade, um pool pode ter sua política de criação de tarefas.

Margaret Bloom
fonte
16

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 awaitsozinho 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.

Andrew Savinykh
fonte
3
Não é uma competição em primeiro lugar; é uma colaboração!
Eric Lippert
16

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:

  • processamento que acontece na CPU
  • processamento que ocorre em outros processadores (GPU, placa de rede etc.), vamos chamá-los de E / S.

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:

  • se eu usar o método Write do FileStreamobjeto (que é um Stream), o processamento será, 1% de CPU associado e 99% de IO.
  • se eu usar o método Write do NetworkStreamobjeto (que é um Stream), o processamento será, 1% de CPU associado e 99% de IO.
  • se eu usar o método Write do Memorystreamobjeto (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 Streamobjeto, 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:

  • Em um aplicativo de desktop, quero imprimir um documento, mas não quero esperar por ele.
  • Meu servidor da web atende muitos clientes ao mesmo tempo, cada um recebendo suas páginas em paralelo (não serializadas).

Antes de assíncrono / espera, tínhamos basicamente duas soluções para isso:

  • Tópicos . Era relativamente fácil de usar, com as classes Thread e ThreadPool. Threads são limitados apenas à CPU .
  • O modelo de programação assíncrono "antigo" Begin / End / AsyncCallback . É apenas um modelo, não informa se você estará vinculado à CPU ou IO. Se você der uma olhada nas classes Socket ou FileStream, é vinculado à IO, o que é legal, mas raramente a usamos.

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.

Simon Mourier
fonte
1
Essa é uma resposta bastante boa, usando o theadpool para trabalho vinculado à CPU é ruim no sentido de que os encadeamentos TP devem ser usados ​​para descarregar operações de E / S. O trabalho vinculado à CPU deve estar bloqueando com ressalvas, é claro, e nada impede o uso de vários threads.
Davidcarr 11/1118
3

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.

vaibhav kumar
fonte
Bom resumo, mas acho que deve responder a mais 2 perguntas para fornecer uma imagem completa: 1. Em qual segmento o código esperado é executado? 2. Quem controla / configura o conjunto de encadeamentos mencionado - o desenvolvedor ou o ambiente de tempo de execução?
stojke
1. Nesse caso, principalmente o código esperado é uma operação vinculada à E / S que não usaria threads da CPU. Se desejar usar a espera para operação vinculada à CPU, uma tarefa separada pode ser gerada. 2. O encadeamento no conjunto de encadeamentos é gerenciado pelo agendador de tarefas que faz parte da estrutura do TPL.
vaibhav Kumar
2

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:

while (getMessage(out message)) // pseudo-code
{
   dispatchMessage(message); // pseudo-code
}

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?

void expensiveOperation()
{
    for (int i = 0; i < 1000; i++)
    {
        Thread.Sleep(10);
    }
}

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:

void expensiveOperation()
{
    doIteration(0);
}

void doIteration(int i)
{
    if (i >= 1000) return;
    Thread.Sleep(10); // Do a piece of work.
    postFunctionCallMessage(() => {doIteration(i + 1);}); // Pseudo code. 
}

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 postFunctionCallMessagepseudo-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 Taskclasse. 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). A Taskclasse 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 .ContinueWithbagunça. Então é aqui que o compilador ajuda você. Faz todo esse encadeamento e continuação para você em segundo plano. Quando você diz awaitque 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.

Calmarius
fonte
0

Na verdade, async awaitcadeias 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 awaits. 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.

Steve Fan
fonte
0

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

Blechdose
fonte