Coroutines em PHP
Guia completo sobre coroutines: conceito, implementações com Generators (PHP 5.5+) e Fibers (PHP 8.1+), padrões de concorrência cooperativa
🔄 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
Fiberespecificamente, veja Fibers em PHP 8.1+.
📚 Índice
- O que são Coroutines?
- Coroutines vs Funções Normais
- Generators: Coroutines Antes do PHP 8.1
- Comunicação com Generators
- Fibers: Coroutines Modernas (PHP 8.1+)
- Comparando Generators e Fibers
- Padrões de Coroutines
- Exemplos Práticos
- Quando Usar Cada Abordagem
🎓 O que são Coroutines?
Definição
Uma coroutine é uma função que pode:
- Pausar sua execução voluntariamente
- Retornar controle para quem a chamou
- 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:
- Pausar (
yieldouFiber::suspend()) - Retomar (
next()ouresume()) - Manter estado entre pausas
- 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
- Fibers em PHP 8.1+ - Detalhes sobre a classe Fiber
- PHP Manual: Generators
- PHP Manual: Fibers
- RFC: Fibers
Coroutines são uma ferramenta poderosa para programação assíncrona e eficiente em PHP! 🚀