Olá pessoal!
Hoje você encontrará um texto longo sem figuras (um pouco mais curto do que o original), onde a tese apresentada no cabeçalho é analisada em detalhes. O veterano da Microsoft,
Terry Crowley, descreve a essência da programação assíncrona e explica por que essa abordagem é muito mais realista e apropriada do que síncrona e seqüencial.
Aqueles que desejam ou pensam em escrever um livro que aborda esses tópicos - escreva em um livro pessoal.
Sincronicidade é um mito. Nada acontece instantaneamente. Tudo leva tempo.
Algumas características dos sistemas de computação e ambientes de programação são fundamentalmente baseadas no fato de que os cálculos ocorrem no mundo físico tridimensional e são limitados pelos limites da velocidade da luz e pelas leis da termodinâmica.
Tal enraizamento no mundo físico significa que alguns aspectos não perdem sua relevância, mesmo com o advento de novas tecnologias que oferecem novas oportunidades e acesso a novas fronteiras de produtividade. Eles permanecem válidos porque não são apenas "opções escolhidas durante o design", mas a realidade subjacente do universo físico.
A diferença entre sincronismo e assincronia na linguagem e a criação de sistemas é apenas um aspecto do design que tem fundamentos físicos profundos. A maioria dos programadores começa a trabalhar imediatamente com esses programas e linguagens, onde a execução síncrona está implícita. De fato, isso é tão natural que ninguém nem menciona ou fala diretamente sobre isso. O termo "síncrono" neste contexto significa que o cálculo ocorre imediatamente, como uma série de etapas sucessivas, e nada mais acontece antes de ser concluído. Eu executo
“c = a + b” “x = f(y)”
- e nada mais acontecerá até que esta instrução seja concluída.
Obviamente, nada acontece instantaneamente no universo físico. Todos os processos estão associados a alguns atrasos - você precisa navegar na hierarquia de memória, executar um ciclo do processador, ler informações de uma unidade de disco ou conectar-se a outro nó da rede, o que também causa atrasos na transmissão de dados. Tudo isso é uma consequência fundamental da velocidade da luz e da propagação do sinal em três dimensões.
Todos os processos estão um pouco atrasados, tudo leva tempo. Definindo alguns processos como síncronos, dizemos, em essência, que vamos ignorar esse atraso e descrever nosso cálculo como instantâneo. De fato, em sistemas de computador, uma infra-estrutura séria é frequentemente instalada, o que permite que você continue a usar ativamente o hardware básico, mesmo quando eles tentam otimizar a interface de programação, apresentando os eventos que ocorrem nele como síncronos.
A própria idéia de que a sincronização é fornecida usando um mecanismo especial e está associada a custos pode parecer ilógica para o programador, que está mais acostumado ao fato de que é a assincronia que requer controle externo ativo. De fato, é o que realmente acontece quando uma interface assíncrona é fornecida: a assincronia fundamental genuína se abre para o programador um pouco mais claramente do que antes, e ele precisa processá-la manualmente, em vez de confiar em um programa que poderia fazer isso automaticamente. A provisão direta de assincronia está associada a custos extras para o programador, mas, ao mesmo tempo, permite que você distribua com mais competência os custos e compensações inerentes a essa área, e não deixe isso à mercê de um sistema que equilibraria esses custos e compensações. A interface assíncrona geralmente corresponde com mais precisão a eventos que ocorrem fisicamente no sistema base e, consequentemente, abre possibilidades adicionais de otimização.
Por exemplo, o processador e o sistema de memória são fornecidos com uma infraestrutura justa responsável pela leitura e gravação de dados na memória, levando em consideração sua hierarquia. No nível 1 (L1), o link de cache pode levar vários nanossegundos, enquanto o próprio link de memória deve percorrer todo o caminho através de L2, L3 e da memória principal, o que pode levar centenas de nanossegundos. Se você apenas esperar até que o link da memória seja resolvido, o processador ficará inativo por uma porcentagem significativa do tempo.
Mecanismos sérios são usados para otimizar esses fenômenos: pipelining com uma visão líder do fluxo de comandos, operações múltiplas simultâneas de busca da memória e armazenamento de dados atual, previsão de ramificação e tentativas de otimizar ainda mais o programa, mesmo quando ele pula para outro local da memória, controle preciso das barreiras de memória para garantir que todo esse mecanismo complexo continuará fornecendo um modelo de memória consistente para um ambiente de programação de nível superior. Tudo isso é feito em um esforço para otimizar o desempenho e fazer o uso máximo do hardware para ocultar esses atrasos de 10 a 100 nanossegundos na hierarquia de memória e fornecer um sistema no qual a execução síncrona deve ocorrer, enquanto reduz o desempenho decente do núcleo do processador.
Está longe de estar sempre claro o quão eficazes são essas otimizações para um pedaço de código específico, e muitas vezes são necessárias ferramentas muito específicas para analisar o desempenho para responder a essa pergunta. Esse trabalho analítico é fornecido no desenvolvimento de alguns códigos muito valiosos (por exemplo, no mecanismo de conversão para Excel, algumas opções de compactação no kernel ou caminhos criptográficos no código).
Operações com um atraso mais significativo, por exemplo, a leitura de dados de um disco rotativo, exigem o uso de outros mecanismos. Nesses casos, ao solicitar a leitura do disco do SO, será necessário alternar completamente para outro encadeamento ou processo, e a solicitação síncrona permanecerá não enviada. Os altos custos de troca e suporte desse mecanismo são aceitáveis, pois a latência oculta nesse caso pode atingir vários milissegundos em vez de nanossegundos. Observe: esses custos não são reduzidos à simples alternância entre threads, mas incluem o custo de toda a memória e recursos, que permanecem inativos até a conclusão da operação. Todos esses custos precisam ser fornecidos para fornecer uma interface supostamente síncrona.
Há várias razões fundamentais pelas quais pode ser necessário revelar a assincronia básica real no sistema e para a qual seria preferível usar uma interface assíncrona com um determinado componente, nível ou aplicativo, mesmo levando em consideração a necessidade de lidar diretamente com a complexidade crescente.
Concorrência Se o recurso fornecido for projetado para um verdadeiro paralelismo, a interface assíncrona permitirá que o cliente emita mais naturalmente simultaneamente várias solicitações e gerencie-as, para fazer pleno uso dos recursos básicos.
Transporte . A maneira usual de reduzir o atraso real em alguma interface é garantir que várias solicitações aguardem o envio a qualquer momento (o quanto isso é realmente útil em termos de desempenho depende de onde obtemos a origem do atraso). De qualquer forma, se o sistema estiver adaptado para pipelining, o atraso real poderá ser reduzido por um fator igual ao número de solicitações que aguardam o envio. Portanto, pode levar 10 ms para concluir uma solicitação específica, mas se você escrever 10 solicitações no pipeline, a resposta poderá chegar a cada milissegundo. A taxa de transferência total é uma função do pipelining disponível, e não apenas um atraso de "passagem" por solicitação. Uma interface síncrona que emite uma solicitação e aguarda uma resposta sempre gera um atraso de ponta a ponta mais alto.
Embalagem (local ou remota) . A interface assíncrona fornece naturalmente a implementação de um sistema de empacotamento de consulta, localmente ou em um recurso remoto (nota: nesse caso, o “disco” na outra extremidade da interface de E / S pode ser “remoto”). O fato é que o aplicativo já deve lidar com o recebimento da resposta e, ao mesmo tempo, haverá algum atraso, pois o aplicativo não interromperá o processamento atual. Esse processamento adicional pode ser associado a solicitações adicionais que naturalmente seriam agrupadas.
O lote local pode fornecer uma transferência mais eficiente de séries de solicitações, ou até a compactação e remoção de solicitações duplicadas diretamente na máquina local. Para poder acessar simultaneamente todo um conjunto de solicitações em um recurso remoto, pode ser necessária uma otimização séria. Um exemplo clássico: um controlador de disco reordena uma série de operações de leitura e gravação para tirar proveito da posição da cabeça do disco em uma placa rotativa e minimizar o tempo de alimentação da cabeça. Em qualquer interface de armazenamento de dados operando no nível do bloco, é possível melhorar seriamente o desempenho agrupando uma série de consultas nas quais todas as operações de leitura e gravação caem no mesmo bloco.
Naturalmente, o empacotamento local também pode ser implementado em uma interface síncrona, mas para isso você terá que "esconder a verdade" em grande medida ou empacotar pacotes de programas como um recurso especial da interface, o que pode complicar significativamente todo o cliente. Um exemplo clássico de ocultar a verdade é a E / S em buffer. O aplicativo chama
“write(byte)”
e a interface retorna com
success
, mas, na verdade, o próprio registro (assim como as informações sobre se foram passados com êxito) não ocorrerá até que o buffer seja explicitamente preenchido ou vazio, e isso acontece quando o arquivo é fechado . Muitos aplicativos podem ignorar esses detalhes - uma bagunça ocorre apenas quando o aplicativo precisa garantir algumas seqüências de operações em interação, além de uma verdadeira idéia do que está acontecendo nos níveis mais baixos.
Desbloquear / Liberar . Um dos usos mais comuns da assincronia no contexto das interfaces gráficas do usuário é impedir que o encadeamento principal da interface do usuário seja bloqueado para que o usuário possa continuar interagindo com o aplicativo. Atrasos nas operações de longo prazo (como comunicações de rede) não podem ser ocultados atrás de uma interface síncrona. Nesse caso, o encadeamento da interface com o usuário deve gerenciar explicitamente essas operações assíncronas e lidar com a complexidade adicional introduzida no programa.
A interface do usuário é apenas um exemplo em que o componente deve continuar a responder a solicitações adicionais e, portanto, não pode contar com algum mecanismo padrão que oculte atrasos para simplificar o trabalho do programador.
Um componente de servidor da Web que recebe novas conexões com soquetes, por via de regra, transfere muito rapidamente essa conexão para outro componente assíncrono que fornece comunicação no soquete e volta a processar novas solicitações.
Nos modelos síncronos, os componentes e seus modelos de processamento geralmente estão intimamente relacionados.
As interações assíncronas são um mecanismo
frequentemente usado para afrouxar a ligação .
Redução e gerenciamento de custos. Como mencionado acima, qualquer mecanismo para ocultar a assincronia envolve alguma alocação de recursos e sobrecarga. Para uma aplicação específica, essa sobrecarga pode não ser aceitável, e o designer dessa aplicação deve encontrar uma maneira de controlar a assincronia natural.
Um exemplo interessante é a história dos servidores web. Os servidores Web antigos (criados no Unix) geralmente usavam um processo separado para gerenciar as solicitações recebidas. Então esse processo pôde ler essa conexão e escrever nela, aconteceu, em essência, de forma síncrona. Esse design se desenvolveu e os custos foram reduzidos quando os encadeamentos começaram a ser usados, em vez de processos, mas o modelo de execução síncrona geral foi preservado. Nas opções de design moderno, reconhece-se que a atenção principal não deve ser dada ao modelo de cálculo, mas, antes de tudo, à entrada / saída relacionada à leitura e gravação ao trocar informações com um banco de dados, sistema de arquivos ou transmitir informações através de uma rede, enquanto formula uma resposta . Geralmente, filas de trabalho são usadas para isso, nas quais é permitido um certo limite no número de encadeamentos - e, nesse caso, é possível criar mais claramente o gerenciamento de recursos.
O sucesso do NodeJS no desenvolvimento de back-end é explicado não apenas pelo suporte desse mecanismo por parte de vários desenvolvedores de JavaScript que cresceram criando interfaces da Web do cliente. No NodeJS, como no script do navegador, é dada muita atenção ao design de maneira assíncrona, o que combina bem com as opções típicas de carregamento do servidor: o gerenciamento dos recursos do servidor depende principalmente de E / S e não do processamento.
Há outro aspecto interessante: essas compensações são mais explícitas e melhor ajustadas pelo desenvolvedor do aplicativo, se você aderir à abordagem assíncrona. No exemplo com atrasos na hierarquia de memória, o atraso real (medido nos ciclos do processador em termos de uma solicitação na memória) aumentou dramaticamente ao longo de várias décadas. Os desenvolvedores de processadores estão lutando para adicionar novos níveis de cache e mecanismos adicionais que estão cada vez mais pressionando o modelo de memória fornecido pelo processador, para que a aparência do processamento síncrono seja mantida.
A alternância de contexto nas fronteiras da E / S síncrona é outro exemplo em que as compensações reais mudaram drasticamente ao longo do tempo. O aumento dos ciclos do processador é mais rápido do que a luta contra atrasos, e isso significa que agora o aplicativo perde muito mais recursos computacionais, enquanto está ocioso de forma bloqueada, aguardando a conclusão do IO. O mesmo problema relacionado ao custo relativo de compromissos fez com que os projetistas de sistemas operacionais se apegassem a esses esquemas de gerenciamento de memória, que são muito mais semelhantes aos modelos anteriores com troca de processos (onde toda a imagem do processo é carregada na memória, após a qual o processo é iniciado), em vez de trocar páginas. É muito difícil ocultar atrasos que podem ocorrer na borda de cada página. A taxa de transferência total dramaticamente aprimorada obtida com grandes solicitações de E / S sequenciais (em comparação com o uso de solicitações aleatórias) também contribui para essas alterações.
Outros tópicosCancelarO cancelamento é um tópico complexo . Historicamente, os sistemas orientados de forma síncrona fizeram um trabalho ruim com o processamento de cancelamento, e alguns nem sequer deram suporte ao cancelamento. O cancelamento basicamente teve que ser projetado "fora da banda", para uma operação como essa, era necessário chamar um segmento de execução separado. Como alternativa, modelos assíncronos são adequados, onde o suporte ao cancelamento é organizado de maneira mais natural, em particular, é usada uma abordagem tão trivial: ela simplesmente ignora qual resposta finalmente retorna (e se é que retorna). O cancelamento se torna cada vez mais importante quando a variabilidade dos atrasos aumenta e, na prática, a taxa de erros também aumenta - o que fornece uma fatia histórica muito boa demonstrando como nossos ambientes de rede se desenvolveram.
Otimização / Gerenciamento de RecursosO design síncrono, por definição, impõe alguma limitação, impedindo que o aplicativo emita pedidos adicionais até que o pedido atual seja concluído. Em um design assíncrono, a aceleração não acontece por nada, portanto, às vezes, é necessário implementá-la explicitamente. Esta
postagem descreve a situação do Word Web App como um exemplo, em que a transição do design síncrono para o assíncrono causou sérios problemas no gerenciamento de recursos. Se o aplicativo usar uma interface síncrona, é possível que não reconheça que a otimização está implicitamente incorporada no código. Ao remover essa limitação implícita, é possível (ou necessário) organizar o gerenciamento de recursos mais explicitamente.
Eu tive que lidar com isso no começo da minha carreira, quando transportamos um editor de texto da API gráfica síncrona da Sun para o X Windows. Ao usar a API Sun, a operação de renderização era síncrona, para que o cliente não recuperasse o controle até que fosse concluída. No X Windows, uma solicitação gráfica foi despachada de forma assíncrona por uma conexão de rede e, em seguida, executada pelo servidor de exibição (que poderia estar na mesma máquina ou em uma máquina diferente).
Para garantir um bom desempenho interativo, nosso aplicativo deve fornecer algumas renderizações (ou seja, garantir que a linha em que o cursor está atualmente seja atualizada e renderizada) e, em seguida, verificar se há alguma outra entrada do teclado que precise ser lida. , ( , ), , . API. , , - . , . UI , .
, 30 (-, Facebook iPhone ). – ( , ), , . , , .
, . , Microsoft, , API – , , . , , – : «, !» , , .
, . – , . , : , , , . , - . , ,
async/await
. «» , , , JavaScript. : , .
Async/await
, , . . , , , .
. , , . , , , . , , ( !). () , , .
, . , . async/await, , , , .
, , , – . , . – , , , ( , – Word Excel). , , - , , , .
, , , , .
, – . .
Conclusões. – , , . , , , . , ; , .