A abordagem assíncrona da Microsoft é um bom substituto para os objetivos mais comuns da programação multithread: aprimorando a capacidade de resposta com relação às tarefas de E / S.
No entanto, é importante perceber que a abordagem assíncrona não é capaz de melhorar o desempenho nem melhorar a capacidade de resposta em relação às tarefas intensivas da CPU.
Multithreading para capacidade de resposta
Multithreading para capacidade de resposta é a maneira tradicional de manter um programa responsivo durante tarefas pesadas de E / S ou tarefas pesadas de computação. Você salva os arquivos em um encadeamento em segundo plano, para que o usuário possa continuar seu trabalho, sem precisar esperar pelo disco rígido para concluir sua tarefa. O encadeamento IO geralmente bloqueia a espera de que parte de uma gravação seja concluída, portanto, as alternâncias de contexto são frequentes.
Da mesma forma, ao executar um cálculo complexo, você deseja permitir a alternância regular de contexto para que a interface do usuário possa permanecer responsiva e o usuário não acha que o programa travou.
O objetivo aqui não é, em geral, fazer com que vários threads sejam executados em diferentes CPUs. Em vez disso, estamos apenas interessados em fazer com que as alternâncias de contexto ocorram entre a tarefa em segundo plano de longa execução e a interface do usuário, para que ela possa atualizar e responder ao usuário enquanto a tarefa em segundo plano estiver em execução. Em geral, a interface do usuário não consome muita energia da CPU, e a estrutura de segmentação ou o SO geralmente decide executá-los na mesma CPU.
Na verdade, perdemos o desempenho geral devido ao custo extra da alternância de contexto, mas não nos importamos porque o desempenho da CPU não era nosso objetivo. Sabemos que geralmente temos mais energia da CPU do que precisamos e, portanto, nosso objetivo em relação ao multithreading é executar uma tarefa para o usuário sem perder tempo.
A alternativa "assíncrona"
A "abordagem assíncrona" altera essa imagem, habilitando as alternâncias de contexto em um único thread. Isso garante que todas as nossas tarefas sejam executadas em uma única CPU e pode fornecer algumas melhorias modestas de desempenho em termos de menos criação / limpeza de encadeamentos e menos alternâncias reais de contexto entre encadeamentos.
Em vez de criar um novo encadeamento para aguardar o recebimento de um recurso de rede (por exemplo, fazer o download de uma imagem), async
é usado um método, que await
é a imagem que fica disponível e, enquanto isso, gera o método de chamada.
A principal vantagem aqui é que você não precisa se preocupar com problemas de encadeamento, como evitar conflitos, pois não está usando bloqueios e sincronização, e há um pouco menos de trabalho para o programador configurar o encadeamento em segundo plano e voltar no segmento da interface do usuário quando o resultado voltar para atualizar a interface do usuário com segurança.
Eu não examinei muito profundamente os detalhes técnicos, mas minha impressão é que gerenciar o download com uma atividade leve ocasional da CPU se torna uma tarefa não para um encadeamento separado, mas algo mais como uma tarefa na fila de eventos da interface do usuário e quando o o download é concluído, o método assíncrono é retomado a partir dessa fila de eventos. Em outras palavras, await
significa algo semelhante a "verificar se o resultado que eu preciso está disponível, se não, me colocar de volta na fila de tarefas deste encadeamento".
Observe que essa abordagem não resolveria o problema de uma tarefa intensiva de CPU: não há dados a aguardar; portanto, não podemos obter as alternâncias de contexto que precisamos que ocorram sem criar um encadeamento de trabalho em segundo plano real. Obviamente, ainda pode ser conveniente usar um método assíncrono para iniciar o encadeamento em segundo plano e retornar o resultado, em um programa que utiliza amplamente a abordagem assíncrona.
Multithreading for Performance
Como você fala sobre "desempenho", também gostaria de discutir como o multithreading pode ser usado para obter ganhos de desempenho, algo que é totalmente impossível com a abordagem assíncrona de thread único.
Quando você está realmente em uma situação em que não possui energia suficiente da CPU em uma única CPU e deseja usar o multithreading para obter desempenho, na maioria das vezes é difícil. Por outro lado, se uma CPU não tem capacidade de processamento suficiente, também é frequentemente a única solução que pode permitir que seu programa faça o que você gostaria de realizar em um prazo razoável, que é o que faz o trabalho valer a pena.
Paralelismo trivial
Obviamente, às vezes pode ser fácil obter uma aceleração real do multithreading.
Se você tiver um grande número de tarefas independentes de uso intensivo de computação (ou seja, tarefas cujos dados de entrada e saída são muito pequenos em relação aos cálculos que devem ser executados para determinar o resultado), poderá obter uma aceleração significativa com criando um conjunto de encadeamentos (dimensionados adequadamente com base no número de CPUs disponíveis) e fazendo com que um encadeamento mestre distribua o trabalho e colete os resultados.
Multithreading prático para desempenho
Eu não quero me apresentar muito como especialista, mas minha impressão é que, em geral, o multithreading mais prático para o desempenho que acontece hoje em dia é procurar lugares em um aplicativo que tenha paralelismo trivial e usar vários threads colher os benefícios.
Como em qualquer otimização, geralmente é melhor otimizar depois que você cria o perfil do desempenho do programa e identifica os pontos críticos: é fácil desacelerar um programa, decidindo arbitrariamente que essa parte deve ser executada em um segmento e a parte em outro, sem primeiro determinando se as duas partes estão ocupando uma parte significativa do tempo da CPU.
Um encadeamento extra significa mais custos de configuração / desmontagem e mais comutadores de contexto ou mais custos de comunicação entre CPU. Se não estiver fazendo o trabalho suficiente para compensar esses custos em uma CPU separada e não precisar ser um encadeamento separado por motivos de capacidade de resposta, isso atrasará as coisas sem nenhum benefício.
Procure tarefas com poucas interdependências e que estejam ocupando uma parte significativa do tempo de execução do seu programa.
Se eles não tiverem interdependências, é um caso de paralelismo trivial, você pode facilmente configurar cada um com um thread e aproveitar os benefícios.
Se você puder encontrar tarefas com interdependência limitada, para que o bloqueio e a sincronização para troca de informações não as reduzam significativamente, o multithreading pode acelerar o processo, desde que você tome cuidado para evitar os perigos do impasse devido à lógica defeituosa ao sincronizar ou resultados incorretos devido à não sincronização quando necessário.
Como alternativa, alguns dos aplicativos mais comuns para multithreading não procuram (de certo modo) a aceleração de um algoritmo predeterminado, mas sim um orçamento maior para o algoritmo que eles planejam escrever: se você estiver escrevendo um mecanismo de jogo , e sua IA precisa tomar uma decisão dentro da sua taxa de quadros, geralmente você pode dar à sua AI um orçamento maior de ciclo de CPU, se pode fornecer sua própria CPU.
No entanto, certifique-se de criar um perfil dos threads e garantir que eles estejam fazendo o trabalho suficiente para compensar o custo em algum momento.
Algoritmos Paralelos
Também existem muitos problemas que podem ser acelerados usando vários processadores, mas que são monolíticos demais para serem simplesmente divididos entre as CPUs.
Os algoritmos paralelos devem ser cuidadosamente analisados em relação aos seus tempos de execução grandes com relação ao melhor algoritmo não paralelo disponível, pois é muito fácil para o custo de comunicação entre CPUs eliminar todos os benefícios do uso de várias CPUs. Em geral, eles devem usar menos comunicação entre CPU (em termos de grande O) do que usar cálculos em cada CPU.
No momento, ainda é amplamente um espaço para pesquisa acadêmica, em parte por causa da análise complexa necessária, em parte porque o paralelismo trivial é bastante comum, em parte porque ainda não temos tantos núcleos de CPU em nossos computadores que problemas que não pode ser resolvido em um período de tempo razoável em uma CPU pode ser resolvido em um período de tempo razoável usando todas as nossas CPUs.