As Fibers (ou fibras) foram introduzidas no PHP 8.1 como um mecanismo de concorrência cooperativa. Elas permitem que você escreva código que pode ser pausado e retomado, sem bloquear a execução do programa. Isso é especialmente útil para operações de I/O, como requisições HTTP ou leitura de arquivos, onde o tempo de espera pode ser aproveitado para executar outras tarefas.

Diferente de threads ou processos, as fibras são leves e não exigem gerenciamento complexo de memória ou sincronização. Elas são uma alternativa mais simples e eficiente para tarefas que envolvem espera, permitindo que você escreva código assíncrono de forma síncrona, melhorando a legibilidade e a manutenção.

O que são

Uma Fiber é uma função ou método que pode ser pausado em um ponto específico e retomado posteriormente. Quando uma fiber é pausada, o controle é devolvido ao chamador, que pode continuar executando outras tarefas. Quando a fiber é retomada, ela continua de onde parou, mantendo o estado interno.

As fibras são implementadas pela classe Fiber, que fornece métodos como start(), resume(), suspend() e getReturn(). O método suspend() pausa a fiber e retorna o controle ao chamador, enquanto resume() retoma a execução, passando um valor opcional que será retornado pelo suspend().

Exemplo básico:

<?php
$fiber = new Fiber(function (): void {
    echo "Iniciando fiber\n";
    $value = Fiber::suspend('pausa');
    echo "Valor recebido: $value\n";
});

echo "Antes de iniciar\n";
$result = $fiber->start();
echo "Retorno do suspend: $result\n";
$fiber->resume('retomada');
echo "Fim do script\n";
?>

Saída:

Antes de iniciar
Iniciando fiber
Retorno do suspend: pausa
Valor recebido: retomada
Fim do script

Neste exemplo, a fiber começa e imprime "Iniciando fiber", depois chama Fiber::suspend(), que pausa a execução e retorna o valor 'pausa' para o chamador. O script principal continua, imprime o retorno e depois chama resume('retomada'), que retoma a fiber e passa o valor 'retomada' como retorno do suspend(). A fiber então imprime o valor recebido e termina.

Concorrência cooperativa

Concorrência cooperativa é um modelo onde as tarefas voluntariamente cedem o controle ao scheduler, em vez de serem interrompidas pelo sistema operacional. No PHP, as fibras são um exemplo de concorrência cooperativa: você decide quando pausar e retomar cada fiber, permitindo que o código seja executado de forma intercalada sem a complexidade de threads.

Isso é diferente de concorrência preemptiva, onde o sistema operacional decide quando interromper uma tarefa, como em threads. A concorrência cooperativa é mais previsível e mais fácil de raciocinar, pois você sabe exatamente quando uma fiber pode ser pausada.

No PHP, as fibras são executadas no mesmo thread, então não há paralelismo real, mas sim intercalação. Isso é suficiente para muitos cenários de I/O, onde o gargalo é a espera por recursos externos. Enquanto uma fiber aguarda uma resposta, o controle pode ser passado para outra fiber, otimizando o uso do processador.

Um exemplo de concorrência cooperativa com duas fibras:

<?php
function tarefa(string $nome, int $tempo): void {
    echo "Início da tarefa $nome\n";
    sleep($tempo); // simula operação de I/O
    echo "Fim da tarefa $nome\n";
}

$fiber1 = new Fiber(function () { tarefa('A', 2); });
$fiber2 = new Fiber(function () { tarefa('B', 1); });

$fiber1->start();
$fiber2->start();

while (!$fiber1->isTerminated() || !$fiber2->isTerminated()) {
    // Aqui você poderia usar Fiber::suspend() em cada fiber para intercalar execuções
}
?>

Note que este exemplo não usa suspend() explicitamente, mas ilustra como as fibras podem ser intercaladas. Em um cenário real, você usaria suspend() dentro de operações de I/O para permitir que outras fibras rodem.

Casos de uso

As fibras são particularmente úteis em aplicações que lidam com muitas operações de I/O, como:

  • Servidores HTTP: processar múltiplas requisições simultaneamente, sem bloquear o loop principal.
  • Clientes HTTP: fazer várias requisições em paralelo e aguardar todas as respostas.
  • Acesso a bancos de dados: executar múltiplas queries em paralelo, se o driver suportar.
  • Leitura/escrita de arquivos: processar vários arquivos simultaneamente.
  • Web scraping: buscar múltiplas páginas ao mesmo tempo.

Um exemplo prático: fazer múltiplas requisições HTTP de forma assíncrona. Usando a extensão curl com curl_multi é possível, mas com fibras fica mais simples e legível. Exemplo com file_get_contents (simulando uma operação lenta):

<?php
function fetchUrl(string $url): string {
    // Simula uma operação de I/O lenta
    $ch = curl_init($url);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    $response = curl_exec($ch);
    curl_close($ch);
    return $response;
}

$urls = [
    'https://api.example.com/dados1',
    'https://api.example.com/dados2',
    'https://api.example.com/dados3',
];

$fibers = [];
foreach ($urls as $url) {
    $fibers[] = new Fiber(function () use ($url) {
        return fetchUrl($url);
    });
}

// Inicia todas as fibras
foreach ($fibers as $fiber) {
    $fiber->start();
}

// Aguarda todas terminarem e coleta os resultados
$results = [];
foreach ($fibers as $fiber) {
    $results[] = $fiber->getReturn();
}

print_r($results);
?>

Neste exemplo, as três requisições são iniciadas em paralelo, mas como fetchUrl não usa suspend(), elas não são realmente intercaladas. Para uma verdadeira concorrência, você precisaria de um mecanismo que permita pausar durante a espera, como usar stream_select ou uma biblioteca de I/O não bloqueante. As fibras fornecem a primitiva, mas a implementação do I/O não bloqueante fica por sua conta.

vs async de outras linguagens

Em linguagens como JavaScript (Node.js), Python (asyncio) e C# (async/await), o modelo assíncrono é baseado em event loops e promises/tasks. O PHP 8.1 com fibras oferece uma abordagem diferente: em vez de depender de um loop de eventos central, você usa fibras que podem ser pausadas e retomadas manualmente.

Comparação com JavaScript:

  • Em JavaScript, async/await é construído sobre promises, e o loop de eventos gerencia a execução. Você não controla diretamente quando uma promise é resolvida.
  • Em PHP, você controla explicitamente quando uma fiber é pausada (suspend()) e retomada (resume()). Isso dá mais flexibilidade, mas também mais responsabilidade.

Comparação com Python asyncio:

  • Python usa async/await com um event loop que agenda corrotinas. As corrotinas são executadas em um único thread, e o loop controla a alternância.
  • PHP não tem um event loop embutido; você deve implementar sua própria lógica de agendamento ou usar uma biblioteca como ReactPHP ou Amphp, que podem ser integradas com fibras.

Vantagens das fibras:

  • Mais simples e direto para casos específicos.
  • Não requer um loop de eventos global.
  • Pode ser usado em conjunto com código síncrono existente.

Desvantagens:

  • Sem um scheduler automático, você precisa gerenciar manualmente as fibras.
  • Não há paralelismo real (apenas concorrência cooperativa).
  • Integração com I/O não bloqueante é mais trabalhosa.

Exemplo de integração com Amphp (uma biblioteca de programação assíncrona para PHP):

<?php
require 'vendor/autoload.php';

use Amp\Loop;
use Amp\Promise;
use Amp\Delayed;

function asyncTask(string $name, int $delay): Promise {
    return Loop::delay($delay, function () use ($name) {
        echo "Tarefa $name concluída\n";
    });
}

Loop::run(function () {
    $fiber1 = new Fiber(function () {
        yield asyncTask('A', 1000);
    });
    $fiber2 = new Fiber(function () {
        yield asyncTask('B', 500);
    });

    $fiber1->start();
    $fiber2->start();

    while (!$fiber1->isTerminated() || !$fiber2->isTerminated()) {
        // Não faz nada, apenas espera
    }
});
?>

Neste exemplo, as fibras são usadas para iniciar operações assíncronas via Amphp, mas a integração não é perfeita porque as fibras não conhecem o event loop. Na prática, você usaria Amphp diretamente, sem fibras, a menos que precise de um controle mais fino.

Boas práticas

  • Use fibras para operações de I/O que podem ser pausadas, não para tarefas intensivas de CPU.
  • Evite chamar sleep() dentro de uma fiber, pois isso bloqueia o thread inteiro, impedindo outras fibras de rodar.
  • Considere usar bibliotecas como ReactPHP ou Amphp que já implementam um loop de eventos, e combine-as com fibras se necessário.
  • Teste bem o fluxo de pausa/retomada, pois erros podem causar deadlocks.

Referências

Exercícios

  1. Escreva um script que crie uma fiber que imprima números de 1 a 5, pausando após cada número, e o script principal imprima letras de 'a' a 'e' entre cada retomada. Use Fiber::suspend() e resume().
  2. ✓ Resposta:
    <?php
    $fiber = new Fiber(function (): void {
        for ($i = 1; $i <= 5; $i++) {
            echo $i . "\n";
            Fiber::suspend();
        }
    });
    
    $fiber->start();
    foreach (range('a', 'e') as $letra) {
        echo $letra . "\n";
        if (!$fiber->isTerminated()) {
            $fiber->resume();
        }
    }
    ?>
  3. Crie uma função que use uma fiber para simular uma operação de I/O com sleep() (apenas para teste) e mostre que o script principal continua executando enquanto a fiber dorme.
  4. ✓ Resposta:
    <?php
    $fiber = new Fiber(function (): void {
        echo "Início da operação\n";
        sleep(2); // Simula I/O
        echo "Fim da operação\n";
    });
    
    $fiber->start();
    echo "Script principal continua...\n";
    // Aguarda a fiber terminar
    while (!$fiber->isTerminated()) {
        // Poderia fazer outra coisa aqui
    }
    ?>
  5. Explique a diferença entre concorrência cooperativa e preemptiva, e como as fibras se encaixam nesse contexto.
  6. ✓ Resposta: Concorrência cooperativa é quando as tarefas cedem o controle voluntariamente, enquanto na preemptiva o sistema operacional interrompe a tarefa. As fibras são cooperativas, pois você decide quando pausar.
  7. Escreva um código que use duas fibras para simular duas tarefas que alternam a execução, cada uma imprimindo uma mensagem e suspendendo.
  8. ✓ Resposta:
    <?php
    $fiber1 = new Fiber(function (): void {
        for ($i = 1; $i <= 3; $i++) {
            echo "Fiber 1 - $i\n";
            Fiber::suspend();
        }
    });
    
    $fiber2 = new Fiber(function (): void {
        for ($i = 1; $i <= 3; $i++) {
            echo "Fiber 2 - $i\n";
            Fiber::suspend();
        }
    });
    
    $fiber1->start();
    $fiber2->start();
    
    while (!$fiber1->isTerminated() || !$fiber2->isTerminated()) {
        if (!$fiber1->isTerminated()) $fiber1->resume();
        if (!$fiber2->isTerminated()) $fiber2->resume();
    }
    ?>
  9. Pesquise sobre a extensão Swoole ou ReactPHP e descreva como elas podem ser usadas com fibras para criar um servidor HTTP assíncrono.
  10. ✓ Resposta: Swoole é uma extensão que fornece um servidor HTTP assíncrono com corrotinas. Você pode usar fibras para controlar a execução dentro de corrotinas. ReactPHP é uma biblioteca que implementa um event loop; você pode combinar fibras com ReactPHP para pausar e retomar tarefas dentro do loop. Exemplo com ReactPHP:
    // Requer composer require react/http
    $server = new React\Http\Server(function (Psr\Http\Message\ServerRequestInterface $request) {
        $fiber = new Fiber(function () use ($request) {
            // Faz algo assíncrono
            return new React\Http\Response(200, ['Content-Type' => 'text/plain'], 'Olá!');
        });
        $fiber->start();
        return $fiber->getReturn();
    });