Como PHP me ajudou a entender computação assíncrona a fundo
PHP é uma das linguagens que move a Web e tem sido meu estudo de caso nos últimos meses por uma série de motivos, mas o principal: async não é uma palavra reservada.
Pode parecer um motivo bobo, certo? Afinal, de que interessa entender a natureza de uma palavra que é mais utilizada no mundo JavaScript do que declaradores de variáveis?
Bom, um ótimo primeiro motivo começa com esse post bem aqui. Mas mais que isso, gosto de entender quais são as peças que compõem coisas do dia a dia.
O que é Execução Assíncrona
A programação assíncrona é uma técnica que permite ao seu programa iniciar uma tarefa potencialmente de longa duração e ainda ser capaz de responder a outros eventos enquanto essa tarefa é executada, em vez de ter que esperar até que a tarefa seja concluída. Assim que a tarefa for concluída, seu programa verá o resultado. - MDN
O modelo MAIS UTILIZADO para relatar uma execução assíncrona: O RESTAURANTE.
No restaurante, em um modelo assíncrono temos alguns atores importantes: um garçom registra os pedidos, o cozinheiro prepara os pedidos que foram levados e avisa quando cada tarefa/pedido foi preparado para que sejam levados aos clientes. Parece simples, certo? Neste modelo, o nosso garçom seria o Loop de Eventos, os pedidos seriam tasks assíncronas e o cozinheiro seria nosso sistema operacional.
Este é um modelo que é nativo do JavaScript com a engine V8 do node, mas também que vem sendo implementado com bibliotecas como asyncio no Python e que, no PHP, temos alguns expoentes que compartilham o mesmo Loop de Eventos, como AMPHP e ReactPHP.
Ocorre que por anos eu trabalhei com esses elementos de forma transparante e, embora saiba como, quando e porquê, ainda não sabia microscopicamente o desempenho de cada peça até montar eu próprio meu quebra-cabeça.
E por que eu faria isso? Porque eu quero conhecer meu sistema: cada parte individualmente tem uma razão de existir e coletivamente possuem outras funções macroscópicas.
Dito isso, uma confissão: eu entendia muito superficialmente programação concorrente (ou async/await para os menos familiarizados com o termo). Até alguns anos atrás, estas palavras reservadas mágicas eram, realmente, palavras reservadas mágicas, cujo entendimento era a nível de execução, mas não de funcionamento. Ora, se eu preciso fazer algo não bloqueante, só preciso fazer uma função assíncrona para que o loop de eventos não seja bloqueado, certo?
Booom... não. O buraco é muito mais embaixo. Duas coisas muito importantes são pouco comentadas quando falamos sobre os "poderes mágicos da programação concorrente":
- Funções assíncronas são executadas uma a uma A NÃO SER QUE DELEGUEM O FLUXO DE CONTROLE - o que é a execeção, não a regra.
- Funções assíncronas são basicamente generators, máquinas de estado que vão sequencialmente retornando valores e podem ser suspensas, resumidas e canceladas.
Em vias práticas, a proposição do RFC de Fibers no PHP 8.1 me ajudou a entender de maneira clara como Python, Kotlin, Java, JavaScript e a engine do Unity utilizam esse conceito de execução assíncrona.
Um loop de eventos fica, dentro da aplicação principal, executando todas as tarefas que estão na fila de execução com base no tempo. Geralmente uma palavra reservada async significa que o evento deve ocorrer QUASE imediamente, ou seja, no próximo tick do loop - no JS isso seria como um setTimeout(()=> {}, 0), porque o setTimeout com um retorno de chamada e zero como segundo argumento agendará o retorno de chamada para ser executado de forma assíncrona, após o menor atraso possível, a fins de demonstração. Mas isso ocorre apenas se o controle for retornado para o event loop, e se você não entende como é fácil bloquear o loop, sugiro esse artigo aqui do próprio Node. Enquanto a próxima função é agendada, se ela tiver alguma operação IO, como ler um arquivo, enviar uma requisição para uma API ou simplesmente usar um backdoor para enviar seus dados para a NSA, ela suspende o próprio fluxo para dizer pro event loop que ele pode fazer outra coisa enquanto isso. E essa, sim, é a parte mágica e complexa.
No ambiente JS e Python, principalmente, estas execuções são quase invisíveis, a não ser que você, assim como eu, seja extremamente curioso e vá procurar na fonte dos executores o que tem feito o que. Via de regra, contamos com promises e futures para orquestrar o resultado que ainda vai vir e com o uso de algum await marcamos que a sequência daquela função vai ocorrer em outro momento quando o resultado da promise ou future tiver sido concluída.
Em PHP, que é o que me levou a escrever este artigo, isso fica mais evidente com as Fibers.
$fiber = new Fiber(function(): void {
echo "Hello from the Fiber...\n";
Fiber::suspend();
echo "Hello again from the Fiber...\n";
});
echo "Starting the program...\n";
$fiber->start();
echo "Taken control back...\n";
echo "Resuming Fiber...\n";
$fiber->resume();
echo "Program exits...\n";
No exemplo anterior, uma fiber é um tipo especial de Green Thread, uma thread a nível de usuário que é resumida pelo próprio usuário. Isso faz com que possamos a qualquer momento suspender uma execução para dar vez a outra, o que é bastante útil quando estamos esperando uma operação de IO. O que, porém, é invisível é que estas threads ficam em revezamento constante até que uma delas continue por tempo maior um processamento CPU-bound.
Lembra aquela cena do Burro em Shrek?
Exatamente isso que uma Green Thread emula. Constantemente retorna o controle para o fluxo principal, dividindo a execução de CPU, enquanto o programa principal questiona por meio de um await se aquele pedaço de código foi finalizado para que possa progredir a partir daquele ponto ou dar sequência a outros.
E como o PHP tem me ajudado a entender melhor tudo isso?
Criando meus próprios componentes.
Nos últimos quatro meses, paralelo ao meu trabalho pessoal, tenho tirado tempo para estudar a fundo tudo isso de maneira mais empírica. Claro que eu poderia usar algo já feito, mas a sensação de reinventar a roda é bem divertida. No meu caso, foquei inicialmente no estudo de streams, que são uma forma de conectar IO ao sistema operacional sem "travar" o fluxo de dados principal da aplicação. É assim, inclusive, que eu implementei do zero (e com muita pesquisa e inspirações) o protocolo de WebSockets.
No final do dia, a mágica na programação é mais um conceito que podemos buscar a fundo para melhorar habilidades já existentes e tirar um tempinho proveitoso para descobrir algo (não tão) novo.