🔄 Coroutines em PHP

Coroutines (corrotinas) são funções que podem ser pausadas e retomadas, permitindo pausar a execução no meio do processamento, fazer outras coisas, e depois voltar exatamente de onde parou. Este conceito é fundamental para programação assíncrona e concorrência cooperativa.

💡 Nota: Este guia cobre o conceito amplo de coroutines. Para detalhes sobre a classe Fiber especificamente, veja Fibers em PHP 8.1+.


📚 Índice

  1. O que são Coroutines?
  2. Coroutines vs Funções Normais
  3. Generators: Coroutines Antes do PHP 8.1
  4. Comunicação com Generators
  5. Fibers: Coroutines Modernas (PHP 8.1+)
  6. Comparando Generators e Fibers
  7. Padrões de Coroutines
  8. Exemplos Práticos
  9. Quando Usar Cada Abordagem

🎓 O que são Coroutines?

Definição

Uma coroutine é uma função que pode:

  1. Pausar sua execução voluntariamente
  2. Retornar controle para quem a chamou
  3. Retomar de onde parou quando chamada novamente

Analogia do Mundo Real

Imagine que você está lendo um livro:

Função normal = Você DEVE ler do início ao fim sem parar Coroutine = Você pode colocar um marcador, fazer outra coisa, e depois voltar ao marcador

<?php
// Função Normal - executa até o fim
function lerCapitulo() {
    echo "Página 1\n";
    echo "Página 2\n";
    echo "Página 3\n";
    // Não pode pausar!
}

lerCapitulo(); // Lê tudo de uma vez

// Coroutine - pode pausar
function lerComPausas() {
    echo "Página 1\n";
    yield; // PAUSA - coloca marcador
    echo "Página 2\n";
    yield; // PAUSA - coloca marcador
    echo "Página 3\n";
}

$leitura = lerComPausas();
$leitura->current(); // Lê página 1 e pausa
// ... faz outras coisas ...
$leitura->next();    // Retoma e lê página 2
$leitura->next();    // Retoma e lê página 3

// Saída esperada
// Página 1
// Página 2
// Página 3
// Página 1
// Página 2
// Página 3

🔄 Coroutines vs Funções Normais

Funções Normais

<?php
function normal() {
    echo "Linha 1\n";
    echo "Linha 2\n";
    echo "Linha 3\n";
    return "Fim";
}

// Executa TUDO de uma vez
$result = normal();
echo $result . "\n";

// Saída esperada
// Linha 1
// Linha 2
// Linha 3
// Fim

Características:

  • ✅ Simples e diretas
  • ❌ Não podem pausar
  • ❌ Não podem retomar
  • ✅ Retornam um único valor

Coroutines (com Generators)

<?php
function coroutine() {
    echo "Linha 1\n";
    yield;
    echo "Linha 2\n";
    yield;
    echo "Linha 3\n";
    return "Fim";
}

// Executa EM ETAPAS
$gen = coroutine();

$gen->current();  // "Linha 1" e pausa
echo "--- Fazendo outras coisas ---\n";

$gen->next();     // "Linha 2" e pausa
echo "--- Fazendo outras coisas ---\n";

$gen->next();     // "Linha 3"
$result = $gen->getReturn();
echo $result . "\n";

// Saída esperada
// Linha 1
// --- Fazendo outras coisas ---
// Linha 2
// --- Fazendo outras coisas ---
// Linha 3
// Fim

Características:

  • ✅ Podem pausar (yield)
  • ✅ Podem retomar (next())
  • ✅ Mantêm estado entre pausas
  • ✅ Podem gerar múltiplos valores

🌱 Generators: Coroutines Antes do PHP 8.1

Antes do PHP 8.1, os Generators (introduzidos no PHP 5.5) eram a forma nativa de criar coroutines.

Criando um Generator

<?php
function contador() {
    for ($i = 1; $i <= 5; $i++) {
        echo "Gerando: $i\n";
        yield $i; // PAUSA e retorna $i
        echo "Retomado após $i\n";
    }
}

$gen = contador();

foreach ($gen as $numero) {
    echo "Recebi: $numero\n";
    echo "---\n";
}

// Saída esperada
// Gerando: 1
// Recebi: 1
// ---
// Retomado após 1
// Gerando: 2
// Recebi: 2
// ---
// Retomado após 2
// Gerando: 3
// Recebi: 3
// ---
// Retomado após 3
// Gerando: 4
// Recebi: 4
// ---
// Retomado após 4
// Gerando: 5
// Recebi: 5
// ---
// Retomado após 5

Generators como Coroutines

<?php
function tarefa() {
    echo "Tarefa: Passo 1\n";
    yield;

    echo "Tarefa: Passo 2\n";
    yield;

    echo "Tarefa: Passo 3\n";
}

$t = tarefa();

// Controle manual da execução
echo "Main: Iniciando tarefa\n";
$t->current(); // Executa até primeiro yield

echo "Main: Fazendo outras coisas\n";

echo "Main: Retomando tarefa\n";
$t->next(); // Executa até segundo yield

echo "Main: Mais coisas\n";

echo "Main: Finalizando tarefa\n";
$t->next(); // Executa até o fim

// Saída esperada
// Main: Iniciando tarefa
// Tarefa: Passo 1
// Main: Fazendo outras coisas
// Main: Retomando tarefa
// Tarefa: Passo 2
// Main: Mais coisas
// Main: Finalizando tarefa
// Tarefa: Passo 3

Exemplo: Fibonacci com Generator

<?php
function fibonacci() {
    $a = 0;
    $b = 1;

    yield $a;
    yield $b;

    while (true) {
        $next = $a + $b;
        yield $next;

        $a = $b;
        $b = $next;
    }
}

// Gera os primeiros 10 números
$fib = fibonacci();
$count = 0;

echo "Fibonacci: ";
foreach ($fib as $numero) {
    echo "$numero ";

    if (++$count >= 10) {
        break;
    }
}
echo "\n";

// Saída esperada
// Fibonacci: 0 1 1 2 3 5 8 13 21 34

Yield com Chave e Valor

<?php
function userIds() {
    yield 'admin' => 1;
    yield 'editor' => 2;
    yield 'viewer' => 3;
}

foreach (userIds() as $role => $id) {
    echo "$role: $id\n";
}

// Saída esperada
// admin: 1
// editor: 2
// viewer: 3

📡 Comunicação com Generators

Generators suportam comunicação bidirecional: podemos enviar valores DE VOLTA para o generator!

Enviando Valores com send()

<?php
function echo_generator() {
    while (true) {
        // Recebe valor de send()
        $received = yield;

        if ($received === null) {
            break;
        }

        echo "Recebi: $received\n";
    }
}

$gen = echo_generator();
$gen->current(); // Inicia o generator

$gen->send("Olá");
$gen->send("Mundo");
$gen->send("PHP");
$gen->send(null); // Encerra

// Saída esperada
// Recebi: Olá
// Recebi: Mundo
// Recebi: PHP

Recebendo e Enviando Simultaneamente

<?php
function doubler() {
    while (true) {
        $value = yield; // Recebe

        if ($value === null) {
            break;
        }

        $doubled = $value * 2;
        yield $doubled; // Envia de volta
    }
}

$gen = doubler();
$gen->current(); // Inicia

// Envia 5, avança, recebe resultado
$gen->send(5);
echo "Resultado: " . $gen->current() . "\n";

// Envia 10, avança, recebe resultado
$gen->next();
$gen->send(10);
echo "Resultado: " . $gen->current() . "\n";

$gen->send(null);

// Saída esperada
// Resultado: 10
// Resultado: 20

Exemplo: Sistema de Tarefas

<?php
function task_scheduler() {
    $tasks = [];

    while (true) {
        $command = yield count($tasks); // Retorna qtd de tarefas

        if ($command === null) {
            break;
        }

        [$action, $data] = $command;

        if ($action === 'add') {
            $tasks[] = $data;
            echo "-> Tarefa adicionada: $data\n";
        } elseif ($action === 'list' && sizeof($tasks) > 0) {
            echo "Tarefas: " . implode(', ', $tasks) . "\n";
        } elseif ($action === 'clear') {
            $tasks = [];
            echo "-> Tarefas limpas\n";
        }
    }
}

$scheduler = task_scheduler();
$scheduler->current(); // Inicia

echo "Total: " . $scheduler->send(['add', 'Estudar PHP']) . "\n";
echo "Total: " . $scheduler->send(['add', 'Fazer exercícios']) . "\n";
$scheduler->send(['list', null]);
$scheduler->send(['clear', null]);
echo "Total: " . $scheduler->send(['list', null]) . "\n";

$scheduler->send(null); // Encerra

// Saída esperada
// -> Tarefa adicionada: Estudar PHP
// Total: 1
// -> Tarefa adicionada: Fazer exercícios
// Total: 2
// Tarefas: Estudar PHP, Fazer exercícios
// -> Tarefas limpas
// Total: 0

🧵 Fibers: Coroutines Modernas (PHP 8.1+)

Com o PHP 8.1, temos Fibers: uma implementação mais poderosa e flexível de coroutines.

Fiber Básica

<?php
$fiber = new Fiber(function() {
    echo "Fiber: Passo 1\n";
    Fiber::suspend();

    echo "Fiber: Passo 2\n";
    Fiber::suspend();

    echo "Fiber: Passo 3\n";
});

echo "Main: Iniciando fiber\n";
$fiber->start();

echo "Main: Retomando fiber\n";
$fiber->resume();

echo "Main: Última retomada\n";
$fiber->resume();

echo "Main: Concluído\n";

// Saída esperada
// Main: Iniciando fiber
// Fiber: Passo 1
// Main: Retomando fiber
// Fiber: Passo 2
// Main: Última retomada
// Fiber: Passo 3
// Main: Concluído

Comunicação com Fibers

<?php
$fiber = new Fiber(function() {
    $name = Fiber::suspend("Como você se chama?");
    echo "Olá, $name!\n";

    $age = Fiber::suspend("Qual sua idade?");
    echo "Você tem $age anos.\n";

    return "Prazer em conhecer você!";
});

$fiber->start();

$question = $fiber->resume("João");
echo "Pergunta: $question\n";

$fiber->resume(25);
echo "Fim!\n";

// Saída esperada
// Olá, João!
// Pergunta: Qual sua idade?
// Você tem 25 anos.
// Fim!

Exemplo: Múltiplas Fibers Concorrentes

<?php
function runConcurrent(array $fibers): void {
    // Inicia todas
    foreach ($fibers as $fiber) {
        $fiber->start();
    }

    // Loop enquanto houver fibers ativas
    $active = true;
    while ($active) {
        $active = false;

        foreach ($fibers as $fiber) {
            if ($fiber->isSuspended()) {
                $fiber->resume();
                $active = true;
            }
        }
    }
}

$fiber1 = new Fiber(function() {
    for ($i = 1; $i <= 3; $i++) {
        echo "Fiber A: $i\n";
        Fiber::suspend();
    }
});

$fiber2 = new Fiber(function() {
    for ($i = 1; $i <= 3; $i++) {
        echo "Fiber B: $i\n";
        Fiber::suspend();
    }
});

runConcurrent([$fiber1, $fiber2]);

// Saída esperada
// Fiber A: 1
// Fiber B: 1
// Fiber A: 2
// Fiber B: 2
// Fiber A: 3
// Fiber B: 3

⚖️ Comparando Generators e Fibers

Tabela Comparativa

Característica Generators Fibers
Versão PHP 5.5+ 8.1+
Palavra-chave yield Fiber::suspend()
Tipo retornado Generator Fiber
Pode ser aninhado ⚠️ Limitado ✅ Sim
Controle fino ❌ Não ✅ Sim
Performance ✅ Boa ✅ Melhor
Uso típico Iteração preguiçosa Concorrência cooperativa

Exemplo Lado a Lado

Com Generator

<?php
function genCounter() {
    for ($i = 1; $i <= 3; $i++) {
        yield $i;
    }
}

$gen = genCounter();
foreach ($gen as $num) {
    echo "Generator: $num\n";
}

// Saída esperada
// Generator: 1
// Generator: 2
// Generator: 3

Com Fiber

<?php
function fiberCounter() {
    return new Fiber(function() {
        Fiber::suspend(1);
        Fiber::suspend(2);
        Fiber::suspend(3);
    });
}

$fiber = fiberCounter();
$fiber->start();

echo "Inicializando...\n";
while (!$fiber->isTerminated()) {
    $num = $fiber->resume();
    if ($num !== null) {
        echo "Fiber: $num\n";
    }
}

// Saída esperada
// Inicializando...
// Fiber: 2
// Fiber: 3

### Quando Usar Cada Um?

**Use Generators quando:**
- ✅ Precisar iterar sobre dados preguiçosamente
- ✅ Trabalhar com grandes conjuntos de dados
- ✅ Implementar pipelines de transformação
- ✅ Código precisa rodar no PHP 5.5+

**Use Fibers quando:**
- ✅ Precisar de concorrência cooperativa
- ✅ Implementar agendadores/event loops
- ✅ Controle fino sobre suspensão/retomada
- ✅ PHP 8.1+ está disponível

---

## 🎨 Padrões de Coroutines

### Padrão 1: Pipeline de Processamento (Generator)

```php
<?php
function readLines(string $file): Generator {
    $handle = fopen($file, 'r');

    while ($line = fgets($handle)) {
        yield trim($line);
    }

    fclose($handle);
}

function filterEmpty(Generator $input): Generator {
    foreach ($input as $line) {
        if ($line !== '') {
            yield $line;
        }
    }
}

function toUpperCase(Generator $input): Generator {
    foreach ($input as $line) {
        yield strtoupper($line);
    }
}

// Uso: pipeline encadeado
// $lines = toUpperCase(filterEmpty(readLines('arquivo.txt')));
// 
// foreach ($lines as $line) {
//     echo "$line\n";
// }

Padrão 2: Produtor-Consumidor (Generator)

<?php
function producer(): Generator {
    for ($i = 1; $i <= 5; $i++) {
        echo "[Produtor] Gerando item $i\n";
        yield $i;
        echo "[Produtor] Item $i foi consumido\n";
    }
}

function consumer(Generator $producer): void {
    foreach ($producer as $item) {
        echo "[Consumidor] Processando item $item\n";
        usleep(100000); // 100ms de processamento
    }
}

consumer(producer());

// Saída esperada
// [Produtor] Gerando item 1
// [Consumidor] Processando item 1
// [Produtor] Item 1 foi consumido
// [Produtor] Gerando item 2
// [Consumidor] Processando item 2
// [Produtor] Item 2 foi consumido
// [Produtor] Gerando item 3
// [Consumidor] Processando item 3
// [Produtor] Item 3 foi consumido
// [Produtor] Gerando item 4
// [Consumidor] Processando item 4
// [Produtor] Item 4 foi consumido
// [Produtor] Gerando item 5
// [Consumidor] Processando item 5
// [Produtor] Item 5 foi consumido

Padrão 3: Agendador de Tarefas (Fiber)

<?php
class TaskScheduler {
    private array $tasks = [];

    public function addTask(callable $callback): void {
        $this->tasks[] = new Fiber($callback);
    }

    public function run(): void {
        // Inicia todas
        foreach ($this->tasks as $task) {
            $task->start();
        }

        // Executa até todas terminarem
        while ($this->hasActiveTasks()) {
            foreach ($this->tasks as $task) {
                if ($task->isSuspended()) {
                    $task->resume();
                }
            }
        }
    }

    private function hasActiveTasks(): bool {
        foreach ($this->tasks as $task) {
            if (!$task->isTerminated()) {
                return true;
            }
        }
        return false;
    }
}

$scheduler = new TaskScheduler();

$scheduler->addTask(function() {
    echo "Task 1: Início\n";
    Fiber::suspend();
    echo "Task 1: Meio\n";
    Fiber::suspend();
    echo "Task 1: Fim\n";
});

$scheduler->addTask(function() {
    echo "Task 2: Início\n";
    Fiber::suspend();
    echo "Task 2: Fim\n";
});

$scheduler->run();

// Saída esperada
// Task 1: Início
// Task 2: Início
// Task 1: Meio
// Task 2: Fim
// Task 1: Fim

💡 Exemplos Práticos

Exemplo 1: Lendo CSV Linha por Linha (Generator)

<?php
function readCsv(string $filename): Generator {
    $handle = fopen($filename, 'r');

    // Lê cabeçalho
    $header = fgetcsv($handle);

    // Lê cada linha como array associativo
    while ($row = fgetcsv($handle)) {
        yield array_combine($header, $row);
    }

    fclose($handle);
}

// Uso com memória constante, mesmo para arquivos gigantes
// foreach (readCsv('usuarios.csv') as $usuario) {
//     echo "Nome: {$usuario['nome']}, Email: {$usuario['email']}\n";
// }

Exemplo 2: Range Personalizado (Generator)

<?php
function range_step(int $start, int $end, int $step = 1): Generator {
class AsyncTimer {
    private array $sleeping = [];

    public function sleep(float $seconds): void {
        $fiber = Fiber::getCurrent();
        if ($fiber === null) {
            throw new RuntimeException("sleep() deve ser chamado em uma Fiber");
        }

        $this->sleeping[] = [
            'fiber' => $fiber,
            'wakeAt' => microtime(true) + $seconds
        ];

        Fiber::suspend();
    }

    public function tick(): void {
        $now = microtime(true);

        foreach ($this->sleeping as $i => $sleep) {
            if ($now >= $sleep['wakeAt']) {
                $sleep['fiber']->resume();
                unset($this->sleeping[$i]);
            }
        }

        $this->sleeping = array_values($this->sleeping);
    }

    public function hasSleeping(): bool {
        return count($this->sleeping) > 0;
    }
}

// Exemplo de uso
$timer = new AsyncTimer();

$fiber1 = new Fiber(function() use ($timer) {
    echo "[" . date('H:i:s') . "] Fiber 1: início\n";
    $timer->sleep(1);
    echo "[" . date('H:i:s') . "] Fiber 1: após 1s\n";
});

$fiber2 = new Fiber(function() use ($timer) {
    echo "[" . date('H:i:s') . "] Fiber 2: início\n";
    $timer->sleep(0.5);
    echo "[" . date('H:i:s') . "] Fiber 2: após 0.5s\n";
});

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

while ($timer->hasSleeping()) {
    $timer->tick();
    usleep(10000); // 10ms
}

// Saída esperada (horários variam)
// [19:32:58] Fiber 1: início
// [19:32:58] Fiber 2: início
// [19:32:59] Fiber 2: após 0.5s
// [19:32:59] Fiber 1: após 1s

🎯 Quando Usar Cada Abordagem

Funções Normais

<?php
// ✅ Use quando:
function calcularMedia(array $numeros): float {
    return array_sum($numeros) / count($numeros);
}

// - Lógica é simples e direta
// - Não precisa pausar
// - Processa tudo de uma vez

Generators (Coroutines Leves)

<?php
// ✅ Use quando:
function processarArquivoGrande(string $arquivo): Generator {
    $handle = fopen($arquivo, 'r');
    while ($linha = fgets($handle)) {
        yield processarLinha($linha);
    }
    fclose($handle);
}

// - Processar grandes volumes de dados
// - Iteração preguiçosa (lazy)
// - Economizar memória
// - Pipeline de transformações
// - PHP 5.5+ disponível

Fibers (Coroutines Poderosas)

<?php
// ✅ Use quando:
$fiber = new Fiber(function() use ($scheduler) {
    $data = $scheduler->httpGet('https://api.com/data');
    $scheduler->sleep(1);
    $result = processData($data);
    return $result;
});

// - Concorrência cooperativa
// - Implementar event loops
// - Agendadores de tarefas
// - Múltiplas operações I/O simultâneas
// - PHP 8.1+ disponível

📊 Resumo Comparativo

Generators

Sintaxe: yield Retorno: Generator Desde: PHP 5.5

Prós:

  • ✅ Disponível em versões antigas do PHP
  • ✅ Perfeito para iteração preguiçosa
  • ✅ Economia de memória
  • ✅ Sintaxe simples

Contras:

  • ❌ Controle limitado
  • ❌ Não ideal para concorrência complexa

Fibers

Sintaxe: Fiber::suspend() Retorno: Fiber Desde: PHP 8.1

Prós:

  • ✅ Controle total sobre execução
  • ✅ Ideal para concorrência
  • ✅ Comunicação bidirecional poderosa
  • ✅ Base para frameworks assíncronos

Contras:

  • ❌ Requer PHP 8.1+
  • ❌ Curva de aprendizado maior

🎓 Conceitos Fundamentais

O que é uma Coroutine?

Função que pode:

  1. Pausar (yield ou Fiber::suspend())
  2. Retomar (next() ou resume())
  3. Manter estado entre pausas
  4. Comunicar bidirecionalmente

Por que usar Coroutines?

  • Memória: Processar grandes dados sem carregar tudo
  • Concorrência: Múltiplas tarefas "simultâneas"
  • Controle: Pausar e retomar quando necessário
  • Eficiência: Melhor uso de recursos I/O

Quando NÃO usar?

  • ❌ Lógica simples e direta
  • ❌ Tarefas CPU-bound (use processos/threads)
  • ❌ Quando adiciona complexidade desnecessária

🔗 Recursos Relacionados


Coroutines são uma ferramenta poderosa para programação assíncrona e eficiente em PHP! 🚀