Skip to content

Instantly share code, notes, and snippets.

@marcelgsantos
Created March 20, 2022 18:55
Show Gist options
  • Star 8 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save marcelgsantos/1f13e04ad6e78accb6d734ea86a7c797 to your computer and use it in GitHub Desktop.
Save marcelgsantos/1f13e04ad6e78accb6d734ea86a7c797 to your computer and use it in GitHub Desktop.
Anotações da palestra 'PHP Assíncrono com Swoole' apresentada pelo Leo Cavalcante

1. Anotações

  • É difícil definir o que é assíncrono.
  • O termo assíncrono é sobre o que não é, isto é, não ser síncrono.
  • Isso é devido a etimologia da palavra. A letra a refere-se a negação ou privação.
  • O termo síncrono refere-se a processos que acontecem um após o outro.
  • Por que não querer que algo seja síncrono? É para obter concorrência e paralelismo.
  • Os termos concorrência e paralelismo referem-se a coisas que acontecem ao mesmo tempo.
  • Nem sempre precisamos que as coisas aconteçam uma após a outra.
  • O fogão de cozinha, por exemplo, possuem várias bocas para que seja possível cozinharmos várias coisas de uma vez.
  • E isso é resolvido na computação usando threads e processos.
  • Uma thread é uma linha de execução dentro de um processo.
  • O termo concorrência refere-se a troca rápida de linhas de execução e tem-se a impressão de que as coisas acontecem ao mesmo tempo mas, na verdade, apenas uma coisa é executada por vez pelo processador.
  • Em um processo pode-se ter uma ou mais threads.
  • Recomenda-se utilizar analogias para compreender a programação assíncrona.
  • Uma analogia bastante conhecida é a do garçom no restaurante.
  • A jornada normal de um garçom dentro do restaurante é a seguinte:
    1. receber cliente
    2. levar até uma mesa livre ou reservada
    3. anotar o pedido
    4. solicitar o prato pra cozinha
    5. levar o prato pronto da cozinha até o cliente
  • Se considerarmos o garçom síncrono os passos serão o seguinte:
    1. receber cliente
    2. levar até uma mesa livre ou reservada
    3. esperar o cliente escolher
    4. anotar o pedido
    5. solicitar o prato pra cozinha
    6. esperar a cozinha fazer o prato
    7. levar o prato pronto da cozinha até o cliente
  • Ter um garçom síncrono funciona somente para um cliente por vez no restaurante e isso é inviável.
  • E, para resolver esse problema, deve-se ter um garçom assíncrono. Isto é, enquanto um cliente escolhe o prato, o garçom atende outro cliente.
  • Se considerarmos o garçom assíncrono os passos serão o seguinte:
    1. receber cliente
    2. levar até uma mesa livre ou reservada
    3. já vai atender outro cliente
    4. anotar o pedido
    5. solicitar o prato pra cozinha
    6. atender um novo cliente ou levar um prato pronto
    7. levar o prato pronto da cozinha até o cliente
  • Um garçom assíncrono faz coisas de forma concorrente, isto é, ele não está fazendo as coisas ao mesmo tempo e sim trocando de contexto várias vezes.
  • Uma pessoa multitarefa consegue trocar de foco várias vezes e trabalha de forma concorrente.
  • No decorrer da evolução da computação começaram a surgir processadores com vários núcleos.
  • Contudo, existem aplicações que utilizam apenas um núcleo do processador enquanto os outros ficam ociosos.

imagem 01 - meme de um trabalhador executando a tarefa enquanto os outros estão ociosos em alusão aos processadores com vários núcleos

  • Como solucionar esse problema de CPUs ociosas? Criar inúmeras threads e processos?
  • As threads e processos foram criados para permitir a execução concorrente mas nem sempre são as melhores opções.
  • Ao utilizar threads, pode-se ter os seguintes problemas:
    • as trocas de contexto entre threads não são operações leves
    • as threads compartilham memória com outras threads em um mesmo processo e isso é pode acarretar problemas como condições de corrida e deadlocks pela necessidade de uso de mutexes
  • Ao utilizar processos, pode-se ter os seguintes problemas:
    • Os processos são muito pesados.
    • Quando se faz um fork de um processo filho ele utiliza toda memória do processo pai.
    • Neste caso, a criação de muitos processos podem consumir muito recurso da máquina.
  • Os processos são úteis em alguns cenários como, por exemplo, criar um processo para cada aba de um navegador.
  • Mas, para outros cenários, é uma operação cara criar inúmeros processos.
  • O Apache utilizava uma abordagem em que um processo pai criava processos filhos e cada um deles tinha uma pool de threads para lidar com a requisição.
  • O PHP-FPM utiliza a abordagem de ter um pool de processos do interpretador PHP para lidar com as requisições.
  • Com o passar do tempo, a web evoluiu e passou-se a ter gargalo de requisições.
  • Isso levou ao problema C10K cunhado por Dan Kegel em 1999.
  • Esse problema propunha que um servidor deveria aguentar 10.000 requisições dentro de uma mesma máquina.
  • "It's time for web servers to handle ten thousand clients simultaneously, don't you think? After all, the web is a big place now." - Dan Kegel
  • Em 2010 o WhatsApp alcançou a marca de lidar com 2 milhões de requisições em uma única máquina com 24 cores utilizando Erlang.
  • A linguagem e a máquina virtual do Erlang inspiraram a Swoole.
  • A linguagem Erlang foi criada pela Ericsson há mais de 30 anos atrás.
  • Em 2002 o russo Igor Sysoev resolveu o problema do C10K.
  • Ele observou que a web é sobre I/O, isto é, unix sockets, sockets TCP e UDP, ler e salvar arquivos, pipes (stdin, stdout e stderr) e dispositivos.
  • O conceito de I/O não tem nada a ver com computação pesada e cálculos matemáticos.
  • Quando uma operação de I/O é iniciada no sistema operacional ela é identificada em uma tabela no kernel com um id conhecido como file descriptor.
  • Sempre que um processo de I/O é iniciado, tem-se no kernel um file descriptor correspondente a esse processo de I/O que está sendo executado.
  • Uma ideia é ler o status do file descriptor dentro do kernel para saber se ele estava pronto, isto é, se um arquivo já foi lido ou se já foram recebidos os pacotes TCP.
  • A leitura do file descriptor não é suficiente pois trata-se ainda de uma operação bloqueante e era necessário uma operação não bloqueante.
  • Uma ideia era fazer a chamada e não bloquear e, então, ficar chamando o kernel para ver se o arquivo está pronto para a leitura.

imagem 02 - técnica de polling para saber se o I/O está pronto

  • Isto é, é possível implementar I/O não bloqueante com as chamadas de sistema select() e poll().
  • Contudo, fazer chamadas de sistema não é uma operação barata.
  • A mecânica de ficar chamando algo para verificar se está pronto é chamada de pooling, logo a chamada de sistema no Linux é a poll().
  • O que resolveu o problema C10K foi o I/O multiplexing e não as abordagens com as chamadas de sistema select() e poll().
  • O I/O multiplexing é utiliza event loop e é controlado por chamadas de sistema mais eficientes como a epoll() que tem complexidade O(1).
  • Existem implementações para outros sistemas operacionais que são eficientes como a IOCP para Windows e kqueue para Mac e FreeBSD.
  • Na biblioteca de epoll existe uma chamada de sistema epoll_wait() que recebe um file descriptor e espera um evento acontecer.
  • Essa é uma abordagem eficiente pois em vez de ficar perguntando toda vez para o kernel, quando algo acontecer o kernel notifica.
  • A utilização da epoll() resolveu o problema C10K e resolve até hoje problemas similares.
  • O Nginx utilizou a ideia de I/O multiplexing para permitir um maior desempenho.
  • O fluxo de funcionamento do Nginx é o seguinte:
    • os clientes enviam requisições e recebem respostas do event loop do Nginx
    • o event loop é single threaded, não bloqueante e responsável por delegar operações lentas e de I/O para workers
    • os workers processam essas operações e retornam para o event loop que, por sua vez, retorna para os clientes

imagem 03 - funcionamento interno do Nginx

  • O conceito de programação orientado a eventos já não era nenhuma novidade devido a softwares desktop que lidavam com eventos como clique de botão.
  • A novidade foi ter um event loop na frente de um I/O multiplexing e ficou conhecido como Reactor Pattern.
  • As ferramentas que implementam o Reactor Pattern são o Tornado de Python e o Node.js de JavaScript.
  • O Node.js surgiu em 2009 e foi importantíssimo para o ecossistema JavaScript.
  • Na mesma época, o Tianfeng Han em Xangai, implementou as mesmas ideias na linguagem PHP quando teve um problemas com inbound de e-mail.
  • A Swoole é uma runtime de I/O não bloqueante para resolver problemas de assincronia.
  • A palavra Swoole vem da palavra sword pois é rápido e preciso como uma espada ou fast and sharp em inglês.
  • A trajetória do projeto Swoole é a seguinte:
    • 2012
      • o projeto ficou open-source no GitHub
    • 2013
      • saiu a versão 1 do Swoole
    • 2016
      • saiu a versão 2 do Swoole com corrotinas
    • 2017
      • a Transfon adotou o projeto e começou a patrociná-lo
      • o patrocínio permitiu que os desenvolvedores dedicassem mais tempo para o projeto
      • houve uma "revolução" na ferramenta
      • não teve a versão 3 (foi da 2 para a 4)
      • o módulo de corrotinas foi reescrito
    • 2018
      • o módulo de corrotinas da v4 utilizou a biblioteca de corrotinas da Tencent (libco)
      • a Tencent é a maior empresa da Ásia
      • a Swoole utiliza a biblioteca de corrotinas (libco) da Tencent e a Tencent utiliza a Swoole
      • a Tencent é a empresa por trás da QQ, WeChat, Epic, Miniclip e Riot (LoL e Valorant)
      • existe game server da Epic implementado em Swoole
      • a Baidu usa Swoole
      • existe uma página dos cases no site
  • A Convenia utilizou a Swoole e teve ganhos incríveis em desempenho e redução de custos na nuvem.
  • O Victor Gazoti escreveu um artigo com sobre o processo de implementação usando Swoole.
  • O nome do artigo é How we increased our PHP app performance by 80% with Laravel and Swoole - Victor Gazoti.
  • O Swoole é multithreaded e aproveitar melhor o poder computacional da máquina semelhante ao Erlang.
  • O Swoole possui os seguintes componentes:
    • Reactor - é o padrão para ler I/O
    • Worker - administram tarefas I/O bound
    • TaskWorker - administram tarefas CPU bound

imagem 04 - arquitetura interna do Swoole

  • Algumas runtimes como a Swoole entregam os recursos da máquina para a máquina virtual e possibilitam a criação de "threads internas" (que não são kernel threads).
  • Essa abordagem é conhecida como m:n threading.
  • As green threads ou lightweight threads são as threads internas criadas por uma runtime como a BEAM do Erlang que é responsável pelo mapeamento com as threads do sistema operacional.
  • A Erlang chama de processo uma unidade de computação que é executada de forma assíncrona.
  • Pode-se utilizar fibers e generators para lidar com programação assíncrona.
  • A Swoole escolheu utilizar corrotinas para lidar com uma unidade de computação que executa de forma assíncrona.
  • As corrotinas são estruturas que cooperam entre si, isto é, são processos que podem se iniciar, parar, dar a vez para outra e assim por diante.
  • A linguagem Go possui o conceito de corrotinas.
  • A Swoole é uma extensão do PHP e pode ser instalada utilizando o comando pecl install swoole.
  • A Swoole utiliza as chamadas de sistema epoll e kqueue, isto é, pode ser utilizada no Linux e no Mac.
  • Ela não funciona no Windows mas pode ser utilizada com Docker e WSL.
  • A linguagem Go inspirou a Swoole e utiliza-se a função go() para executar uma corrotina.
// example 1a - anonymous function
go(function() {
    echo 'Hello, Swoole!';
});

// example 1b - arrow function
go(fn () => print('Hello, Swoole!'));

// example 1c - normal function
function greet() {
    echo 'Hello, Swoole!';
}

go(fn () => greet());
// example 2a
function something_slow()
{
    sleep(1);
}

// run synchronously and takes 3s
something_slow();
something_slow();
something_slow();
// example 2b
Swoole\Runtime::enableCoroutine();

function something_slow()
{
    sleep(1);
}

// run asynchronously and takes 1s
go('something_slow');
go('something_slow');
go('something_slow');
  • A instrução Swoole\Runtime::enableCoroutine() transforma o funcionamento interno do PHP de síncrono para assíncrono.
  • Ela faz um hijacking de funcionalidades internas fazendo-as executar dentro de corrotina.
  • O hijacking é feito na php stream que é uma construção da Zend VM e tudo que a utiliza vai executar dentro de corrotinas como MySQLi, PDO e a extensão de Redis.
  • O hijacking também habilita o funcionamento de corrotinas em extensões que são bastante utilizadas como o Curl.
  • O Curl é, na verdade, uma extensão PHP que faz o binding para a libcurl em C.
  • Pode-se utilizar Communicating Sequential Process ou CSP para resolver a comunicação entre as unidades de processamento assíncrona.
  • O Communicating Sequential Process sugiu de um paper de Sir Tony Hoare, cientista da computação incrível e responsável pela criação do null.
  • As corrotinas se comunicam através de message passing.
  • Os channels são uma implementação de message passing e são utilizadas na Swoole e em Go.
  • Existem outras APIs para trabalhar com corrotinas como waitgroup, barrier, batch e parallel.
  • O async e await são um açúcar sintático para trabalhar com programação assíncrona.
  • Pode-se utilizar um preemptive scheduler para orquestrar as corrotinas.
  • Pode-se habilitar um preemptive scheduler na Swoole.
Swoole\Runtime::enableScheduler();
ini_set("swoole.enable_preemptive_scheduler", "1");
  • A Swoole, apesar de influências de Go e Erlang, tem uma fundação forte e caminha com as próprias pernas. A inclusão do preemptive scheduler, por exemplo, foi feita antes mesmo de Go.
  • Existe uma Awesome List de Swoole com inúmeras referências como bindings de Mezzio e Laravel para Swoole.

2. Dúvidas

  • O que é um unix socket?
  • O que é um pool de threads e processos?
  • O que spawn de processos?
  • O que é uma syscall ou chamada de sistema?
  • O que é I/O multiplexing?
  • O que é CPU bound, IO bound e memory bound?
  • O que é green thread?
  • O que é corrotina?
  • O que é hijacking?
  • O que é communicating sequential process?

3. Referências

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment