Coroutines no PHP 5.5

Artigo que descreve a utilização de coroutines no PHP 5.5.

Introdução

No último post, vimos sobre Generators, que é um dos novos recursos do PHP 5.5. Neste artigo, veremos um recurso complementar, chamado de Coroutine, que permite "consumir" uma função generator.

Como funcionam as Coroutines

Coroutines são criadas de forma similar às funções generator. Também é utilizada a palavra reservada yield, porém, num contexto diferente. Enquanto os generators utilizavam o yield para "retornar" uma chave/valor, as coroutines utilizam o yield para receber um valor e realizar alguma operação com ele. Veja um exemplo de uma coroutine:

function gravar_log() {
    $handle = fopen('exemplo.log', 'a');
    if (!$handle) {
        throw new RuntimeException('Erro ao abrir arquivo de log');
    }
    for (;;) {
        fwrite($handle, yield . PHP_EOL);
    }
}

Explicando: a função abre o arquivo com a flag "append" (adicionar conteúdo ao final), fica em um loop infinito gravando o valor recebido pela coroutine no arquivo.

Como a função acima possui a palavra chave yield, então ela retorna um objeto da classe Generator. Esta classe, além de implementar os métodos definidos pela interface Iterator, também possui o método send, que é utilizado pelas Coroutines. Portanto, para utilizar a função coroutine, basta utilizar o método send conforme o exemplo:

$logger = gravar_log();
$logger->send('Linha 1');
$logger->send('Linha 2');
$logger->send('Linha 3');

Ao chamar o método send pela primeira vez, a função coroutine irá começar sua execução pela primeira vez, até chegar à linha do yield, ou seja, o arquivo só será aberto ao chamar o send pela primeira vez. Depois, nas próximas chamadas ao send, a função continua a execução do ponto onde havia parado.

Exceções em Coroutines

Ao lançar a versão Alpha 3, foi introduzido um outro método à classe Generator, que é o método throw. Ele pode ser usado para o utilizador da função conseguir soltar uma exceção dentro da coroutine. Neste caso, a exceção pode ser capturada/tratada pela coroutine. O método recebe por parâmetro a exception a ser disparada contra a coroutine. Veja o exemplo:

function gravar_log() {
    $handle = fopen('exemplo.log', 'a');
    if (!$handle) {
        throw new RuntimeException('Erro ao abrir arquivo de log');
    }
    try {
        for (;;) {
            fwrite($handle, yield . PHP_EOL);
        }
    } catch (Exception $e) {
        // abortar gravacao
    } finally {
        fclose($handle);
    }
}

// Usando a coroutine
$gen = gravar_log();
$gen->send('linha 1');
$gen->send('linha 2');
$gen->throw(new Exception('Erro')); // disparando a excecao
$gen->send('linha 3');

Neste exemplo, lançamos uma exceção dentro da coroutine, no ponto em que a função havia parado, ou seja, após a instrução que usou yield. Neste caso, o arquivo gerado no exemplo terá o conteúdo:

linha 1
linha 2

Note que ele não incluiu a "linha 3" pois a exceção disparada abortou a gravação após o disparo. Como não há nenhum yield após o bloco try/catch/finally, na coroutine, então a instrução $gen->send('linha 3') foi simplesmente ignorada.

Dica: como as coroutines normalmente estão em um loop infinito, não adianta colocarmos códigos abaixo do loop pois nunca se chegará até eles (exceto se o loop for interrompido). Então se precisamos fazer alguma operação após utilizarmos a coroutine (por exemplo: fechar o arquivo que foi aberto), podemos criar um bloco try/finally, colocando o loop dentro do try e as operações finais dentro do finally. Assim, quando o script for encerrado, serão executadas as instruções do bloco finally. Observe, porém, que não adianta colocar instruções abaixo do bloco finally, pois elas não seráo executadas.

Observações

Utilizar coroutines pode trazer alguns benefícios em performance, embora possam gastar um pouco mais de memória, dependendo de como são usadas. No exemplo mostrado, tem melhor performance pois só precisa abrir o arquivo uma única vez, mas, por outro lado, precisa armazenar o "handle" do arquivo em memória. Se a função coroutine grava muitas variáveis, elas ficam em memória até serem explicitamente desalocadas (com unset) ou o objeto generator seja desalocado (também com unset).

Coroutines permitem algumas aplicações práticas bastante sofisticadas, como mostrado nesta apresentação sobre coroutines. Talvez, no futuro, escrevo algum artigo sobre alguma destas aplicações, quando tiver utilizado coroutines na prática.

0 comentários