Threads no PHP com pthreads

Artigo que mostra como utilizar o conceito de threads em PHP, através do módulo pthreads.

Introdução
representação de threads de um processo

Thread é um importante recurso de algumas linguagens de programação, que provê melhoria de performance através da execução de blocos de instruções em paralelo. Por outro lado, ela também exige uma forma própria de pensar na solução dos problemas. Neste artigo, veremos rapidamente o que são threads e um tutorial de como e onde elas podem poderão ser usadas com PHP.


O que são threads?

Antes de responder a esta pergunta, é preciso dizer que existem dois tipos de threads: Kernel Level Thread (KLT) e User Level Thread (ULT). Neste artigo, vamos nos concentrar apenas nas threads do tipo ULT, que são as suportadas pelo PHP.

Uma ULT é um conjunto de instruções que pode ser executado em paralelo com outras instruções do mesmo programa. Desta forma, quando temos um dispositivo (computador) com vários processadores ou um processador com vários núcleos (multi-cores), é possível colocar duas (ou mais) threads do mesmo processo rodando em paralelo, cada uma em um processador ou núcleo próprio. O resultado disso é uma potencial melhoria na performance do processo. Ou seja, ao invés de executar instrução por instrução sequencialmente, você consegue executar alguns blocos de instruções em paralelo e, em determinados pontos do código, exigir uma sincronização para garantir que os "resultados" ou processamentos executados pelas tarefas paralelas tenham sido concluídas.

Uma característica importante a se destacar é que as threads compartilham a mesma região de memória do processo que a iniciou. Portanto, várias threads podem trabalhar sobre o mesmo dado da memória. Por fazerem parte de um único processo, utilizar threads normalmente tem melhor performance do que executar vários processos iguais em paralelo. Por outro lado, threads precisam ter algum tipo de controle sobre o acesso ao mesmo dado, já que pode existir acesso concorrente.


Como usar Threads no PHP?

Instalação do pthreads

No PHP, assim como outras linguagens de programação, o suporte a threads é feito através de um módulo (extensão). No caso do PHP, o módulo pthreads é o módulo responsável pelo suporte a threads. Ele é um módulo Pecl, portanto possui boa performance.

Até o momento que este artigo foi escrito, este módulo está em fase Beta, ou seja, ainda não é a versão final estável para uso. Porém, é útil conhecer seus recursos e experimentá-los para tirar proveito dele quando ele estiver mais estável. Certamente é um módulo muito promissor e que, até o momento, tem tido resultados satisfatórios.

Para instalar o módulo pthreads, é necessário que o PHP esteja compilado com suporte a ZTS e isso é feito incluindo a opção --enable-maintainer-zts (ou --enable-zts para Windows) durante a compilação do PHP.

Depois, basta instalar o módulo Pecl pthreads normalmente. Caso tenha dúvidas sobre o que são módulos Pecl e como são instalados, leia o artigo Instalação de módulos Pear e Pecl.

Utilização do pthreads

O módulo pthreads oferece três classes principais para utilização: Thread, Worker e Stackable. Por enquanto, veremos apenas a classe Thread, que já possui muita coisa envolvida. Num próximo artigo falaremos da Worker e Stackable, que são estruturas usadas para casos mais específicos.

A classe Thread permite a criação de uma thread simples. Ela é uma classe abstrata, portanto precisa ser herdada por uma classe filha para ser usada. Ela oferece vários métodos finais (que não podem ser sobrescritos nas classes filhas) e um método abstrato (que obrigatoriamente precisa ser implementado nas classes filhas concretas). O método abstrato chama-se "run" e ele deve agrupar o conjunto de instruções da thread que poderá ser executado em paralelo com outras instruções do programa.

Veja um exemplo simples de 10 threads executadas em paralelo e cada uma terminando em um tempo diferente:

<?php
// Classe que aguarda um tempo aleatorio e depois imprime algo na tela
class AguardaRand extends Thread {

    // ID da thread (usado para identificar a ordem que as threads terminaram)
    protected $id;

    // Construtor que apenas atribui um ID para identificar a thread
    public function __construct($id) { 
        $this->id = $id;
    }

    // Metodo principal da thread, que sera acionado quando chamarmos "start"
    public function run() {
        // Sortear um numero entre 1 e 4
        $tempo_rand = mt_rand(1, 4);

        // Aguardar o tempo sorteado
        sleep($tempo_rand);

        // Imprimir quem e' a thread e quanto tempo ela aguardou
        printf(
            "Sou a thread %d e aguardei %d segundos\n",
            $this->id,
            $tempo_rand
        );
    }
}

/// Execucao do codigo

// Criar um vetor com 10 threads do mesmo tipo
$vetor = array();
for ($id = 0; $id < 10; $id++) {
    $vetor[] = new AguardaRand($id);
}

// Iniciar a execucao das threads
foreach ($vetor as $thread) {
    $thread->start();
}

// Encerrar o script
exit(0);

Observe que chamamos o método start, ao invés de chamar o método run. O método start serve justamente para iniciar a execução da thread, mas continuar a execução das instruções do script corrente. O método run será acionado automaticamente pelo método start (em uma thread de processo diferente), logo você não deve chamar o método run diretamente.

Portanto, o que o script vai fazer é iniciar todas as threads quase que ao mesmo tempo e executá-las quase em paralelo. Digo "quase" porque não tenho 10 processadores, então a execução de cada thread vai se alternar nos processadores disponíveis, conforme a disponibilidade de uso gerenciado pelo sistema operacional, causando a impressão de que executaram em paralelo. Cada thread vai sortear um tempo aleatório para aguardar e depois vai imprimir uma mensagem. O resultado impresso é aleatório, mas o importante destacar aqui é que todas que sortearam o valor "1" vão se encerrar (aproximadamente) após 1 segundo do início da execução das threads; as que sortearam o valor "2" vão se encerrar após 2 segundos do início da execução das threads; e assim sucessivamente. Um exemplo de resultado impresso é este:

Sou a thread 5 e aguardei 1 segundos
Sou a thread 8 e aguardei 1 segundos
Sou a thread 2 e aguardei 2 segundos
Sou a thread 0 e aguardei 3 segundos
Sou a thread 9 e aguardei 3 segundos
Sou a thread 1 e aguardei 4 segundos
Sou a thread 3 e aguardei 4 segundos
Sou a thread 4 e aguardei 4 segundos
Sou a thread 6 e aguardei 4 segundos
Sou a thread 7 e aguardei 4 segundos

Note que, embora as threads tenham sido iniciadas em sequência (1, 2, 3, ...), as threads 5 e 8 foram as primeiras a terminar a execução, pois elas sortearam o valor "1" (aguardaram só um segundo), que era o menor tempo possível que poderia ser sorteado.

Observação: no exemplo anterior, utilizamos 10 instâncias de threads do mesmo tipo. Porém, em situações reais, pode ser que você queira executar threads de tipos diferentes em paralelo. Rodar threads do mesmo tipo é útil quando queremos, por exemplo, realizar operações idênticas sobre elementos de um vetor muito grande. Neste caso, cada thread pode realizar a mesma operação, mas em fatias específicas do vetor.

Com os métodos apresentados (run e start) já conseguiremos fazer o básico, que é colocar instruções para rodar em paralelo. Mas existem situações mais complexas onde queremos executar instruções em paralelo, mas depois precisaremos dos resultados gerados por estas instruções para fazer alguma coisa com eles. Neste caso, precisamos usar o método join no ponto em que queremos garantir que uma determinada thread já tenha terminado.

Veja um exemplo de como usaríamos o método join:

...

// Criar uma thread para calcular alguma coisa em "segundo plano"
$minhaThread = new MinhaThread();

// Iniciar a execucao da thread e seguir com as instrucoes do script
$minhaThread->start();

// Agora fazemos alguma outra coisa enquanto a thread esta sendo executada
fazerAlgumaCoisa();

// Agora queremos usar o resultado do processamento da thread,
// entao precisamos garantir que ela tenha terminado
$minhaThread->join();

// Aqui temos a garantia de que a thread terminou, entao podemos
// pegar o resultado que ela calculou
$resultadoThread = $minhaThread->obterResultado();

// Agora podemos usar o $resultadoThread com seguranca
...

Neste exemplo, imaginemos que a classe MinhaThread calcula alguma coisa e armazena em algum atributo da classe. Depois, o método público obterResultado é responsável por obter o valor do atributo, que guarda o resultado calculado pelo método run.

Se voltarmos ao primeiro exemplo, veremos que não usei o método join. Apenas iniciamos as threads e chamamos o comando exit. Porém, quando o script termina (final do arquivo ou chamada ao comando exit), ele na verdade aguarda o encerramento de todas as threads que foram iniciadas. Se você deseja enviar o pacote HTTP de resposta para o cliente e não quer aguardar as threads terminarem, uma solução é chamar a função fastcgi_finish_request. Só que, como o nome já sugere, esta função só funcionará para instalações do PHP que utilizam FastCGI.

Com o que foi apresentado, dá pra imaginar que a estratégia e modelagem da solução de problemas usando threads é diferente. É preciso avaliar as etapas necessárias para solução do problema e então verificar quais delas podem ser executadas em paralelo. Isso normalmente é feito avaliando se uma etapa depende do resultado de outra. Por outro lado, também existem as etapas que podem ser quebradas em partes menores e as threads realizarem operações similares, mas em fatias de dados específicas, como citado anteriormente.


Conclusão

Eu pretendia escrever um pouco mais sobre Threads, especialmente sobre o acesso concorrente de threads aos dados na memória, mas como o artigo já está começando a ficar muito grande, vou deixar para outra oportunidade. Em todo caso, com o que foi mostrado já é possível fazer alguns experimentos interessantes.

17 comentários

Anônimo disse...

Olá amigo! quando tento instalar o pthreads ocorre o seguinte erro abaixo, tb não consigo instalar nenhum outro pacote pecl, já os pacotes pear instalo normalmente, saberia mim dizer o que ocorre? estou utilizando Win 7 32bits com WampServer 2.2.

C:\>pecl install pthreads
downloading pthreads-0.0.45.tgz ...
Starting to download pthreads-0.0.45.tgz (63,703 bytes)
................done: 63,703 bytes
35 source files, building
ERROR: The DSP pthreads.dsp does not exist.

Rubens Takiguti Ribeiro (autor do blog) disse...

Olá Anônimo,

De fato alguns pacotes pecl não instalam corretamente. Leia o artigo abaixo para mais detalhes:
http://rubsphp.blogspot.com.br/2010/11/instalacao-de-modulos-pear-e-pecl.html

Experimente fazer passo a passo (comandos no linux):
$ pecl download nome_do_pacote
$ tar zxvf pacote_baixado.tgz
$ cd diretorio_descompactado
$ phpize
$ ./configure
$ make
$ make install

No windows não sei como seriam.

Ou então, veja se encontra a dll pronta para Windows.

Sandri disse...

Olá amigo, veja o que acontece comigo. Uso o Webmail AfterLogic http://www.afterlogic.com/webmail-client entao enquanto o php está enviando ou recebendo um email, os outros códigos que tenho no mesmo site/servidor nao funcionam enquanto p php nao termina o enviar/receber.... entao se recebo um email com anexo de 10MB, e abro a tela de clientes, tenho que ficar aguardo o processo anterior terminar.... Esse seu post dá uma solução para esse probleminha?

Rubens Takiguti Ribeiro (autor do blog) disse...

Olá, Sandri.
No seu caso as Threads não devem ajudar, pois se tratam de scripts diferentes. A grosso modo, pode-se dizer que as threads ajudam um script a executar várias tarefas em paralelo. No seu caso, é um sistema com vários scripts e cada script deve funcionar em paralelo aos outros (para isso não há necessidade de Threads).

Pelo que me parece, pela sua descrição, é que a sessão do sistema não está sendo utilizada da melhor forma. O problema é que se um script chama "session_start", outros scripts que abrirem sessão com "session_start" com o mesmo ID de sessão ficarão aguardando até que a sessão seja liberada pelo primeiro script que a abriu.

Caso seja isso mesmo, a melhor forma de resolver é usando a função "session_write_close" nos pontos do código que você já terminou de fazer qualquer escrita em sessão. Se você tem um script que apenas abre a sessão para leitura, você pode chamar "session_start" e "session_write_close" logo em seguida, pois os dados de sessão ficarão disponíveis para leitura em $_SESSION. Mas se você quiser escrever algo em sessão e já tiver chamado "session_write_close", então precisará abrir novamente com "session_start". Enfim, dê uma lida na documentação desta função, que lá também fala sobre esta questão do "data lock" da sessão (trancamento de dados):
http://www.php.net/session_write_close

Além disso, recomendo a leitura destes outros artigos, que tratam especificamente de sessões em PHP:
http://rubsphp.blogspot.com.br/2011/04/sessoes-em-php.html
http://rubsphp.blogspot.com.br/2013/05/session-start-economico.html

leo disse...

Rubens, você poderia explicar passo a passo como utilizar o pthreads no xampp com windows? Quero saber como instalar e se possível com exemplo prático (como o que você deu no texto) já em funcionamento.

obs: Estou pedindo muito, mas procurando na internet, você parece ser o único que conhece isso, nem em inglês eu acho material.

Obrigado

Rubens Takiguti Ribeiro (autor do blog) disse...

Olá, Leo
Infelizmente faz muito tempo que não trabalho com Windows, então é difícil eu te passar as orientações mais precisas.
Em todo caso, no Windows as extensões PECL podem ser instaladas de duas formas: como uma DLL externa ou compilando o PHP com a extensão desejada. Provavelmente os pacotes pré-compilados como o xamp não devem ter incluído esta extensão ao serem compilados, já que ela é muito recente e consequentemente pouquíssimo usada.
Encontrei um artigo em inglês que explica como proceder com instação de módulos PECL no Windows, dê uma olhada lá:
http://www.ksingla.net/2010/05/adding-a-pecl-extension-to-your-php-build-environment/

Abraão Zaidan disse...

Excelente artigo. Eu precisava consultar vários web services ao mesmo tempo (sem usar JavaScript) e coletar o retorno do qual respondesse primeiro, descartando os demais. Virou um "strategy pattern paralelo".

Anônimo disse...

Olá Rubens,

Meu nome é Paulo e primeiramente parabéns pelo Blog, descobri recentemente e achei os artigos de ótima qualidade.

Pegando carona na dúvida do Sandri:
"script deve funcionar em paralelo aos outros"
Como fica a execução do mesmo script simultaneamente?
Tomo como exemplo este script (a.php):



Executando simultaneamente (em 5 abas do navegador mesmo), o resultado que obtive foi o seguinte:

Output 1a execução: 20:00:05
Output 2a execução: 20:00:10
Output 3a execução: 20:00:10
Output 4a execução: 20:00:10
Output 5a execução: 20:00:10

Gostaria de entender melhor por que o tempo foi o mesmo da 2a a 5a execução, e a diferença entre a 1a e a 2a.



Rubens Takiguti Ribeiro (autor do blog) disse...

Olá, Paulo
Seja bem vindo ao blog.
Infelizmente o código que você postou não apareceu aqui nos comentários por conta do sinal de maior e menor, que não são permitidos no blogger. Se puder, envie novamente o código (ou a ideia do que ele faz) para eu entender exatamente seu cenário.
Em todo caso, threads não estão relacionadas à executar o mesmo script em paralelo (por exemplo, em abas diferentes). Como comentei com o Sandri, este tipo de paralelismo quem gerencia é o próprio servidor HTTP em conjunto com o Sistema Operacional. Ou seja, se dois usuários diferentes (ou "duas abas do navegador") acessam o mesmo script PHP, não precisa se preocupar com o acesso concorrente deles através de threads. A não ser que seja um script que utiliza sessão (neste caso, o mesmo usuário não pode executar scripts em paralelo com a sessão aberta, precisa liberá-la antes para o próximo script poder continuar).
As threads só ajudam um script se ele possuir vários blocos de operações que podem ser executados em paralelo. Assim, se o script pedir para executar cada bloco em uma thread, o script como um todo poderá ser executado mais rápido, dependendo da capacidade e disponibilidade dos processadores do servidor. Por exemplo: se o script executa 10 blocos de operações que levam cada uma 1 segundo, então levaria 10 segundos para executá-los em sequência. Mas se executarmos em paralelo, pode ser que leve pouco mais de 1 segundo (dependendo do servidor). Isso é útil para usufruir ao máximo do servidor e deixá-lo livre o quanto antes.

Anônimo disse...

O código era um sleep seguido de echo:
sleep(5);
echo date('H:i:s');

A ideia é apenas testar o acesso concorrente. Imagine 5 usuários acessando o mesmo script simultaneamente. Deveria abrir 5 processos no SO e processar paralelamente certo? Mas pelo meu teste parece que houve uma fila. Esperaram o processo 1 terminar, para então paralelamente executar os processos 2~5.
Gostaria de tentar entender o motivo disto ter acontecido.

Rubens Takiguti Ribeiro (autor do blog) disse...

Olá, Paulo
No seu caso, é o mesmo script rodando em paralelo, ou seja, paralelismo controlado pelo servidor HTTP. Fiz um teste aqui fazendo 9 requisições ao mesmo script (com o conteúdo que você sugeriu) e todos me retornaram exatamente o mesmo conteúdo. Ou seja, a configuração do meu servidor HTTP (apache) suportou 9 acessos simultâneos. Talvez o seu não esteja configurado da mesma forma. Se quer se aprofundar no assunto, sugiro que estude o servidor HTTP.
Eu uso o Apache:
http://httpd.apache.org/docs/current/mod/mpm_common.html#maxrequestworkers

Rubens Takiguti Ribeiro (autor do blog) disse...

Vinícius, esse módulo tornou-se estável em 08/09/2013, que foi um pouco depois da escrita deste artigo, mas já faz muito tempo. Desde então ele já passou para versão 1, 2 e agora está na 3. Confesso que não cheguei a usá-lo em nada importante, e também não conheço nenhum sistema que tenha usado.

No momento, o PHP 7 já está bastante rápido, então é difícil encontrar uma ferramenta Web que precise realmente paralelizar operações para tornar o seu tempo de resposta aceitável (lembrando que pensar em soluções paralelizadas nem sempre é fácil).

Embora seja possível existirem sistemas que precisem rodar bem rápido e paralelizar operações, usar threads nem é a única possibilidade. É possível montar um esquema onde APIs são acionadas em paralelo, e então vários servidores resolvem o problema e depois algum monta o resultado.

Eu diria que o uso deste módulo seria realmente útil se fosse usado de forma inteligente por algum framework, e de forma opcional. Tem várias operações que poderiam ser feitas em paralelo em páginas web. Um exemplo seria gerar algum log ao mesmo tempo em que uma view é renderizada.

Não sei se chegarei a escrever novamente sobre isso, mas quem sabe...

Vinícius Dias disse...

Opa, Rubens.

Eu tenho a seguinte situação: Preciso realizar uma chamada a 3 webservices que me retornam dados para que eu realize esse cálculo. Como está sendo feito hoje: Chama o primeiro, espera o retorno. Chama o segundo, espera o retorno. Chama o terceiro, espera o retorno. Só então, faz o cálculo.

Este é um cenário interessante para se utilizar threads, não acha? Cada thread realizaria uma chamada de webservice...

Abraços!

Rubens Takiguti Ribeiro (autor do blog) disse...

Olá, Vinícius
Existem várias abordagens.
Com pthreads é possível, mas elas só funcionam no php-cli.
Mas você também pode fazer seus webservices serem assíncronos, ou seja, eles recebem a requisição, colocam numa fila de processamento, e já devolvem um resultado de que a operação foi agendada com sucesso. Depois você pode ter end-points para checar qual a situação de cada processamento (se já terminou, e qual foi o resultado). Essa checagem pode ser sequencial, já que você já iniciou todos os webservices e eles devem rodar em paralelo.