Qual é a diferença entre programação assíncrona e multithreading?

234

Eu pensei que eles eram basicamente a mesma coisa - escrever programas que dividem tarefas entre processadores (em máquinas com mais de 2 processadores). Então eu estou lendo isso , que diz:

Os métodos assíncronos devem ser operações sem bloqueio. Uma expressão de espera em um método assíncrono não bloqueia o encadeamento atual enquanto a tarefa aguardada está em execução. Em vez disso, a expressão inscreve o restante do método como uma continuação e retorna o controle para o chamador do método assíncrono.

As palavras-chave assíncronas e aguardam não fazem com que threads adicionais sejam criados. Os métodos assíncronos não requerem multithreading porque um método assíncrono não é executado em seu próprio encadeamento. O método é executado no contexto de sincronização atual e usa o tempo no encadeamento apenas quando o método está ativo. Você pode usar o Task.Run para mover o trabalho vinculado à CPU para um encadeamento em segundo plano, mas um encadeamento em segundo plano não ajuda em um processo que está apenas aguardando a disponibilização dos resultados.

e estou pensando se alguém pode traduzir isso para o inglês para mim. Parece fazer uma distinção entre assincronicidade (isso é uma palavra?) E encadeamento e implica que você pode ter um programa que possui tarefas assíncronas, mas sem multithreading.

Agora entendo a ideia de tarefas assíncronas, como o exemplo na pág. 467 de C # de Jon Skeet em profundidade, terceira edição

async void DisplayWebsiteLength ( object sender, EventArgs e )
{
    label.Text = "Fetching ...";
    using ( HttpClient client = new HttpClient() )
    {
        Task<string> task = client.GetStringAsync("http://csharpindepth.com");
        string text = await task;
        label.Text = text.Length.ToString();
    }
}

A asyncpalavra-chave significa " Esta função, sempre que for chamada, não será chamada em um contexto em que sua conclusão seja necessária para que tudo após a chamada seja chamada".

Em outras palavras, escrevê-lo no meio de alguma tarefa

int x = 5; 
DisplayWebsiteLength();
double y = Math.Pow((double)x,2000.0);

, já DisplayWebsiteLength()que não tem nada a ver com xou y, fará com DisplayWebsiteLength()que seja executado "em segundo plano", como

                processor 1                |      processor 2
-------------------------------------------------------------------
int x = 5;                                 |  DisplayWebsiteLength()
double y = Math.Pow((double)x,2000.0);     |

Obviamente, esse é um exemplo estúpido, mas estou correto ou totalmente confuso ou o quê?

(Além disso, estou confuso sobre o porquê sendere enunca é usado no corpo da função acima.)

user5648283
fonte
13
Esta é uma explicação agradável: blog.stephencleary.com/2013/11/there-is-no-thread.html
Jakub Lortz
sendere eestão sugerindo que esse é realmente um manipulador de eventos - praticamente o único lugar onde async voidé desejável. Provavelmente, isso é chamado com o clique de um botão ou algo parecido - o resultado é que essa ação ocorre completamente de forma assíncrona em relação ao restante do aplicativo. Mas ainda está tudo em um encadeamento - o encadeamento da interface do usuário (com um pequeno intervalo de tempo em um encadeamento IOCP que lança o retorno de chamada no encadeamento da interface do usuário).
Luaan 11/01/16
3
Uma observação muito importante no DisplayWebsiteLengthexemplo de código: Você não deve usar HttpClientem uma usinginstrução - Sob uma carga pesada, o código pode esgotar o número de soquetes disponíveis, resultando em erros de SocketException. Mais informações sobre instanciação imprópria .
Gan #
1
@JakubLortz Não sei para quem é o artigo. Não é para iniciantes, pois requer um bom conhecimento sobre threads, interrupções, coisas relacionadas à CPU etc. Não é para usuários avançados, pois para eles já está claro. Tenho certeza de que isso não ajudará ninguém a entender o que é isso - alto nível de abstração.
Loreno

Respostas:

589

Seu mal-entendido é extremamente comum. Muitas pessoas aprendem que multithreading e assincronia são a mesma coisa, mas não são.

Uma analogia geralmente ajuda. Você está cozinhando em um restaurante. Uma encomenda chega para ovos e torradas.

  • Síncrono: você cozinha os ovos e depois a torrada.
  • Assíncrono, com rosca única: você começa a cozinhar os ovos e define um cronômetro. Você inicia a torrada e define um temporizador. Enquanto ambos estão cozinhando, você limpa a cozinha. Quando os contadores disparam, retira os ovos do fogo e as torradas da torradeira e os serve.
  • Assíncrono, multithread: você contrata mais dois cozinheiros, um para cozinhar ovos e outro para cozinhar torradas. Agora você tem o problema de coordenar os cozinheiros, para que não entrem em conflito um com o outro na cozinha ao compartilhar recursos. E você tem que pagá-los.

Agora, faz sentido que o multithreading seja apenas um tipo de assincronia? Enfiar é sobre trabalhadores; assincronia é sobre tarefas . Nos fluxos de trabalho multithread, você atribui tarefas aos trabalhadores. Nos fluxos de trabalho assíncronos de thread único, você tem um gráfico de tarefas em que algumas tarefas dependem dos resultados de outras; à medida que cada tarefa é concluída, ele invoca o código que agenda a próxima tarefa que pode ser executada, dados os resultados da tarefa recém-concluída. Mas você (espero) precisa apenas de um trabalhador para executar todas as tarefas, não de um trabalhador por tarefa.

Isso ajudará a perceber que muitas tarefas não são vinculadas ao processador. Para tarefas vinculadas ao processador, faz sentido contratar tantos trabalhadores (threads) quantos houver processadores, atribuir uma tarefa a cada trabalhador, atribuir um processador a cada trabalhador e fazer com que cada processador faça o trabalho de nada além de calcular o resultado como o mais rápido possível. Mas para tarefas que não estão aguardando em um processador, você não precisa atribuir um trabalhador. Você apenas espera a chegada da mensagem de que o resultado está disponível e faz outra coisa enquanto espera . Quando essa mensagem chegar, você poderá agendar a continuação da tarefa concluída como a próxima coisa na lista de tarefas a ser desmarcada.

Então, vejamos o exemplo de Jon com mais detalhes. O que acontece?

  • Alguém chama DisplayWebSiteLength. Who? Nós não nos importamos.
  • Ele define um rótulo, cria um cliente e pede que o cliente busque algo. O cliente retorna um objeto que representa a tarefa de buscar algo. Essa tarefa está em andamento.
  • Está em andamento em outro segmento? Provavelmente não. Leia o artigo de Stephen sobre por que não há tópicos.
  • Agora aguardamos a tarefa. O que acontece? Verificamos se a tarefa foi concluída entre o momento em que a criamos e a aguardamos. Se sim, buscamos o resultado e continuamos em execução. Vamos supor que não tenha sido concluído. Registramos o restante deste método como a continuação dessa tarefa e o retorno .
  • Agora o controle retornou ao chamador. O que isso faz? O que ele quiser.
  • Agora, suponha que a tarefa seja concluída. Como isso foi feito? Talvez estivesse em execução em outro encadeamento, ou talvez o chamador para o qual retornamos tenha permitido concluir o encadeamento atual. Independentemente disso, agora temos uma tarefa concluída.
  • A tarefa concluída solicita ao thread correto - novamente, provavelmente o único thread - para executar a continuação da tarefa.
  • O controle passa imediatamente de volta para o método que acabamos de deixar no ponto de espera. Agora não é resultado disponível para que possamos atribuir texte executar o resto do método.

É como na minha analogia. Alguém pede um documento. Você envia pelo correio o documento e continua fazendo outro trabalho. Quando chega no correio, você recebe um sinal e, quando lhe apetece, faz o resto do fluxo de trabalho - abra o envelope, pague as taxas de entrega, qualquer que seja. Você não precisa contratar outro trabalhador para fazer tudo isso por você.

Eric Lippert
fonte
8
@ user5648283: O hardware está no nível errado para pensar em tarefas. Uma tarefa é simplesmente um objeto que (1) representa que um valor ficará disponível no futuro e (2) pode executar código (no encadeamento correto) quando esse valor estiver disponível . Como qualquer tarefa individual obtém o resultado no futuro depende dela. Alguns usarão hardware especial como "discos" e "placas de rede" para fazer isso; alguns usarão hardware como CPUs.
Eric Lippert
13
@ user5648283: Novamente, pense na minha analogia. Quando alguém pede que você cozinhe ovos e torradas, você usa um hardware especial - um fogão e uma torradeira - e pode limpar a cozinha enquanto o hardware está fazendo seu trabalho. Se alguém lhe pedir ovos, torradas e uma crítica original do último filme do Hobbit, você pode escrever sua crítica enquanto os ovos e as torradas estão cozinhando, mas você não precisa usar hardware para isso.
Eric Lippert
9
@ user5648283: Agora, quanto à sua pergunta sobre "reorganizar o código", considere isso. Suponha que você tenha um método P que tenha um retorno de rendimento e um método Q que foreach sobre o resultado de P. Percorra o código. Você verá que executamos um pouco de Q, depois um pouco de P e um pouco de Q ... Você entende o que é isso? aguardar é essencialmente render retorno em fantasias . Agora está mais claro?
Eric Lippert
10
A torradeira é hardware. O hardware não precisa de um thread para atendê-lo; discos e placas de rede e outros enfeites executados em um nível muito inferior ao dos threads do sistema operacional.
precisa
5
@ShivprasadKoirala: Isso absolutamente não é verdade . Se você acredita nisso, então você tem algumas crenças muito falsas sobre assincronia . O ponto principal da assincronia no C # é que ele não cria um encadeamento.
Eric Lippert
27

Javascript no navegador é um ótimo exemplo de um programa assíncrono que não possui threads.

Você não precisa se preocupar com vários trechos de código que tocam nos mesmos objetos ao mesmo tempo: cada função termina a execução antes que qualquer outro javascript possa ser executado na página.

No entanto, ao fazer algo como uma solicitação AJAX, nenhum código está sendo executado; portanto, outro javascript pode responder a coisas como eventos de clique até que a solicitação volte e invoque o retorno de chamada associado a ela. Se um desses outros manipuladores de eventos ainda estiver em execução quando a solicitação AJAX voltar, seu manipulador não será chamado até que seja concluído. Existe apenas um "encadeamento" de JavaScript em execução, embora seja possível pausar efetivamente o que estava fazendo até que você tenha as informações necessárias.

Nos aplicativos C #, o mesmo acontece sempre que você lida com elementos da interface do usuário - você só pode interagir com elementos da interface do usuário quando estiver no thread da interface do usuário. Se o usuário clicar em um botão e você desejar responder lendo um arquivo grande do disco, um programador inexperiente poderá cometer o erro de ler o arquivo no próprio manipulador de eventos click, o que levaria o aplicativo a "congelar" até o o carregamento do arquivo terminou porque não é permitido responder a mais eventos de cliques, pairar ou outros relacionados à interface do usuário até que esse segmento seja liberado.

Uma opção que os programadores podem usar para evitar esse problema é criar um novo encadeamento para carregar o arquivo e informar ao código desse encadeamento que, quando o arquivo é carregado, ele precisa executar o código restante no encadeamento da interface do usuário novamente para atualizar os elementos da interface do usuário. com base no que foi encontrado no arquivo. Até recentemente, essa abordagem era muito popular porque era o que as bibliotecas e a linguagem C # facilitavam, mas é fundamentalmente mais complicado do que precisa ser.

Se você pensar no que a CPU está fazendo quando lê um arquivo no nível do hardware e do sistema operacional, está basicamente emitindo uma instrução para ler partes de dados do disco na memória e atingir o sistema operacional com uma "interrupção". "quando a leitura estiver concluída. Em outras palavras, a leitura do disco (ou de qualquer E / S realmente) é uma operação inerentemente assíncrona . O conceito de um encadeamento aguardando a conclusão da E / S é uma abstração criada pelos desenvolvedores da biblioteca para facilitar a programação. Não é necessário.

Agora, a maioria das operações de E / S no .NET possui um ...Async()método correspondente que você pode chamar, que retorna um Taskquase imediatamente. Você pode adicionar retornos de chamada a isso Taskpara especificar o código que você deseja executar quando a operação assíncrona for concluída. Você também pode especificar em qual encadeamento deseja executar esse código e fornecer um token no qual a operação assíncrona pode verificar periodicamente para ver se você decidiu cancelar a tarefa assíncrona, dando a oportunidade de interromper seu trabalho rapidamente e graciosamente.

Até as async/awaitpalavras - chave serem adicionadas, o C # era muito mais óbvio sobre como o código de retorno de chamada é chamado, porque esses retornos estavam na forma de delegados que você associou à tarefa. Para ainda oferecer o benefício de usar a ...Async()operação, evitando complexidade no código, async/awaitabstrai a criação desses delegados. Mas eles ainda estão lá no código compilado.

Assim, você pode fazer com que o manipulador de eventos da interface do usuário seja awaituma operação de E / S, liberando o encadeamento da interface do usuário para fazer outras coisas e retornando mais ou menos automaticamente ao encadeamento da interface do usuário quando terminar de ler o arquivo - sem precisar crie um novo thread.

StriplingWarrior
fonte
Há apenas um "thread" JavaScript em execução - não é mais verdade com os Web Workers .
Oleksii
6
@oleksii: Isso é tecnicamente verdade, mas eu não entraria nisso porque a própria API Web Workers é assíncrona e os Web Workers não têm permissão para impactar diretamente os valores de javascript ou o DOM na página da Web em que são invocados. from, o que significa que o segundo parágrafo crucial dessa resposta ainda é verdadeiro. Da perspectiva do programador, há pouca diferença entre chamar um Trabalhador da Web e chamar uma solicitação AJAX.
precisa saber é o seguinte