Este artigo é o primeiro de uma sequência que pretendo publicar aqui. Neste entenderemos o que é React PHP e o que nos possibilita.
Outra nota importante é: esta não é uma série introdutória. O foco está em entender como React funciona, deixando de lado introduções didáticas focadas em desenvolver uma aplicação de exemplo.
Visão Geral
A palavra chave em React é assíncrono. Esta é a maior ideia por trás da coleção de bibliotecas que vemos consigo.
PHP, por natureza, é dito “bloqueante”. Isto significa que cada procedimento só virá a ser executado após o anterior. Ilustrado na imagem (obrigado @gabrielcouto) e código:
PHP é bloqueante por natureza. Fonte: http://dev.r3c.com.br/palestra-async-html/#/9, acesso em 24/12/2015 16:58
<?php echo 'Obtendo o arquivo...'; // Executa somente após a linha 3 $conteudo = file_get_contents('umArquivoPesado.txt'); // Executa somente após a linha 5, e assim por diante... if ($conteudo) { echo 'Arquivo obtido com sucesso! :)'; }
Estamos de fato acostumados com este cenário. Porém isto passa a ser problemático quando umArquivoPesado.txt leva alguns segundos para ser carregado à memória.
Esta mesma ideia se aplica às requisições a serviços dentro do código PHP, carregamento de configurações (XML, JSON, YAML…), acesso ao banco de dados e por aí vai. O ponto é: PHP bloqueia a cada comando executado e os comandos que fazem entrada/saída (acesso a disco, acesso a rede…) de dados tendem a ser mais demorados que comandos internos de processamento (um echo, um if, um cálculo…).
React PHP vem com o intuito, justamente, de permitir que executemos pedaços de lógica em paralelo. Nada que não pudesse ser feito com PHP antes, mas React traz uma interface orientada a objetos muito bem organizada e que nos facilita esta utilização.
EventLoop
Para tornar isto possível, React centraliza sua execução em um “EventLoop”, que nos permitirá alcançar a ilustração seguinte:
React não bloqueante. Fonte: http://dev.r3c.com.br/palestra-async-html/#/21, acesso em 24/12/2015 17:23
EventLoop nada mais é que um laço infinito (infinito até que seja interrompido ou que não possua mais processos a executar) de repetição que organiza e elege blocos de código (como são as funções) para execução. Em sua estrutura ele:
- Gerencia processos a executar (funções, callbacks…)
- Identifica atualizações sobre processos em paralelo
- Executa processos referentes aos processos em paralelo
Com este modelo é possível executar procedimentos de entrada/saída (comumente demorados) em paralelo com a execução de tarefas de CPU.
Abaixo exemplifico como o EventLoop gerencia os processos a serem executados:
$loop = ReactEventLoopFactory::create(); $numeros = range(0, 2); $letras = range('A', 'C'); $callback01 = function () use (&$numeros) { echo current($numeros).' '; next($numeros); }; $callback02 = function () use (&$letras) { echo current($letras).' '; next($letras); }; $controle = function () use (&$numeros, &$letras, $loop) { // Se lemos o ultimo número e a ultima letra, pare if (!current($numeros) && !current($letras)) { $loop->stop(); } }; $loop->addPeriodicTimer(1, $callback01); $loop->addPeriodicTimer(1, $callback02); $loop->addPeriodicTimer(1, $controle); // Inicia e executa o EventLoop $loop->run(); // Saída esperada: 0 A B 1 C 2
Acima o EventLoop deverá ter executado, a cada um segundo, os processos $callback01, $callback02 e $controle, sendo que este último identifica que o programa encerrou as leituras necessárias e solicita o fim da execução.
É importante ressaltar que $callback01, $callback02 e $controle não executaram em paralelo, mas o tempo de processamento destes é tão ínfimo que podemos ter esta impressão. Experimente mudar o tempo, em segundos, dos timers (linhas 23 a 25) para visualizar melhor como o EventLoop organiza e elege os processos a serem executados.
Timers
No exemplo de código anterior vimos como funciona o método addPeriodicTimer(), que se fizessemos um paralelo com o JavaScript seria equiparado ao setInterval().
Temos também o equivalente ao setTimeout(), que com o React se refere como addTimer() como segue:
$start = microtime(true); $timeout = $loop->addTimer(3, function () use ($start) { $intervalo = sprintf('%0.2f s', microtime(true) - $start); echo "[{$intervalo}] Timeout veion"; }); $interval = $loop->addPeriodicTimer(2, function () use ($start) { $intervalo = sprintf('%.2f s', microtime(true) - $start); echo "[{$intervalo}] Interval veion"; }); $loop->addTimer(15, function () use ($loop, $interval, $start) { if ($loop->isTimerActive($interval)) { $interval->cancel(); // Alias para $loop->cancelTimer($interval) $intervalo = sprintf('%.2f s', microtime(true) - $start); echo "[{$intervalo}] Interval infinito cancelado.n"; } }); /* Saída esperada: [2.00 s] Interval veio [3.00 s] Timeout veio [4.00 s] Interval veio [6.00 s] Interval veio [8.00 s] Interval veio [10.00 s] Interval veio [12.00 s] Interval veio [14.00 s] Interval veio [15.00 s] Interval infinito cancelado. */
Analisando o código acima, deverá ser impresso uma única vez a frase “Timeout veio” após 3 segundos do início da execução do programa. E deverá ser impressa infinitamente a frase “Interval veio” a cada 2 segundos.
Após 15 segundos de execução do programa realizamos uma ordem de cancelamento para o intervalo infinito caso ele esteja ativo perante o EventLoop. O EventLoop, portanto, não possuirá mais itens na fila de execução e encerra o programa na próxima iteração.
Streams
Até o momento não vimos nenhuma execução que de fato fosse paralela, apenas trabalhamos com filas de execução muito bem organizadas e temporizadas.
O EventLoop traz consigo uma abstração para lidar com Streams que, em php, podem ser trabalhados utilizando wrappers e/ou funções e nenhum dos dois modelos é muito intuitivo.
Precisamos sempre ter em mente que streams são encaixados em dois grupos: readable (de onde lemos dados) e writable (por onde escrevemos dados). Alguns tipos de streams se encaixam, inclusive, nos dois grupos.
O EventLoop tratará Streams utilizando os métodos addReadStream() e addWriteStream(). Abaixo um exemplo de seu funcionamento:
// Iniciando um server em localhost, porta 7171 $serverSock = stream_socket_server('tcp://127.0.0.1:7171'); // Aqui dizemos que ele não bloqueia execução stream_set_blocking($serverSock, 0); // Lista contendo todos os clientes conectados $clients = array(); // Adicionamos um "leitor" que chamará o callback sempre que // $serverSock estiver pronto para leitura (quando alguém se conectar, neste caso) $loop->addReadStream($serverSock, function ($serverSock, $loop) use (&$clients) { $clientSock = stream_socket_accept($serverSock); stream_set_blocking($clientSock, 0); // Vamos identificar nossas conexões para entender melhor... $username = false; // Emite uma mensagem ao $clientSock que acabou de se conectar fwrite($clientSock, "Diga-nos seu nome: "); // Criamos também um buffer de leitura para o $clientSock // Este executa a cada mensagem enviada pelo $clientSock $loop->addReadStream($clientSock, function ($clientSock, $loop) use (&$username, &$clients) { // $username == false -> Ainda não autenticou-se if (!$username && $username = fgets($clientSock)) { $username = trim($username); fwrite($clientSock, "Bem-vindo, {$username}. Você está no chat maroto!nn"); // Adiciona à lista de clients conhecidos $clients[] = $clientSock; } // Se já se identificou e enviou alguma mensagem, repasse if ($username && $text = fgets($clientSock)) { // Busco TODOS os clients conhecidos, e redistribuo // a mensagem $text para todos que não o remetente foreach ($clients as $client) { if ($client !== $clientSock) { fwrite($client, "[{$username}] {$text}"); } } } }); });
Esta aplicação pode ser testada, por exemplo, utilizando o programa “telnet”.
Todo callback passado para addReadStream() será sempre executado quando aquele stream estiver pronto para leitura, e isto varia de acordo com o stream que estiver trabalhando: pode ser uma nova conexão recebida, ou uma mensagem recebida.
De forma análoga, addWriteStream() executará os callbacks assim que o stream estiver preparado para escrita.
Ticks
Por fim, talvez o mais importante, precisamos apresentar os Ticks dentro do EventLoop.
Como dito anteriormente, o mecanismo do EventLoop não passa de um loop infinito. Literalmente, veja este trecho retirado de ReactEventLoopStreamSelectLoop:
public function run() { $this->running = true; while ($this->running) { // Lógica de organização de processos } }
Cada vez que entramos neste laço (linha 05) o EventLoop dispara filas de execução eleitas para aquela iteração (tick).
Na implementação StreamSelectLoop, um tick inicia a execução (nesta ordem):
- Da fila nextTickQueue
- Da fila futureTickQueue
- Dos timers registrados (addTimer() e addPeriodicTimer())
- Dos callbacks registrados para streams (addReadStream() e addWriteStream())
Podemos manipular as filas nextTickQueue e futureTickQueue através dos métodos nextTick() e futureTick(), respectivamente. Eles recebem funções (callable) como parâmetro:
$loop->nextTick(function() { echo "Next tick :Dn"; }); $loop->futureTick(function() { echo "Future tick :Dn"; }); /* Saída esperada: Next tick :D Future tick :D */
Eles realmente parecem fazer a mesma coisa, mas existe uma sutil difereça entre os dois:
- Next ticks executarão enquanto existirem callbacks em sua fila de execução
- Future ticks executarão somente os callbacks existentes na fila no momento em que foram inicializados
O exemplo abaixo ilustra melhor esta diferença:
function futureTick() { echo "Future tickn"; global $loop; $loop->futureTick('futureTick'); $loop->stop(); } $loop->futureTick('futureTick'); // Saída esperada: Future tick
Este future tick envia a mesma função para a fila de future ticks assim que executa, depois solicita o fim do EventLoop. A função futureTick(), porém, executará uma unica vez, pois quando a fila de future ticks iniciou a execução, existia somente uma ocorrência para executar.
Veja a diferença com os next ticks:
function nextTick() { echo "Next tickn"; global $loop; $loop->nextTick('nextTick'); $loop->stop(); } $loop->nextTick('nextTick');
Este programa entrará num loop infinito, pois ao fim de nextTick() esta função é enviada novamente para a fila de next ticks e esta fila executa enquanto existirem callbacks, independentemente do momento em que foram adicionados.
Implementações de EventLoop
Se você notar, no início deste texto, instanciamos o EventLoop utilizando a ReactEventLoopFactory. Isto porque, atualmente, existem quatro implementações diferentes do EventLoop (todas devem respeitar a interface ReactEventLoopLoopInterface) e a Factory irá instanciar o primeiro disponível. São as implementações:
- ExtEventLoop (Utiliza a extensão Event)
- LibEvLoop (Utiliza a extensão Libev)
- LibEventLoop (Utiliza a extensão LibEvent)
- StreamSelectLoop (Não utiliza extensões)
Destas, somente StreamSelectLoop funciona somente com PHP pois faz uso de stream_select(). O restante das implementações somente serão instanciadas quando a devida extensão existir.
Em questão de performance, StreamSelect é a implementação menos perfeita. As outras implementações delegam a gerência de entrada/saída às extensões. A medição de performance, porém, dependerá do seu cenário de uso e, algumas vezes, pode ser mais interessante instanciar o EventLoop manualmente em vez de depender da Factory para escolher o melhor para você.
Conclusão
Vimos aqui as funcionalidades básicas do EventLoop, como ele se comporta e como está desacoplado do restante das bibliotecas React.
Através destas explicações você já será capaz de criar bibliotecas que interajam com o React, assim como desenvolver novas implementações de EventLoops, modificar existentes ou mesmo contribuir com melhorias neste pacote do projeto.
Powered by WPeMatico