Corrotinas

Webman é desenvolvido com base no Workerman, portanto, o Webman pode usar os recursos de corrotinas do Workerman.
As corrotinas suportam três drivers: Swoole, Swow e Fiber.

Pré-requisitos

  • PHP >= 8.1
  • Workerman >= 5.1.0 (composer require workerman/workerman ~v5.1)
  • webman-framework >= 2.1 (composer require workerman/webman-framework ~v2.1)
  • Instalação da extensão swoole ou swow, ou instalação de composer require revolt/event-loop (Fiber)
  • Corrotinas estão desativadas por padrão e precisam ser configuradas individualmente para ativar o eventLoop.

Como ativar

O Webman suporta a ativação de diferentes drivers para diferentes processos, portanto, você pode configurar o driver de corrotina no config/process.php usando a configuração eventLoop:

return [
    'webman' => [
        'handler' => Http::class,
        'listen' => 'http://0.0.0.0:8787',
        'count' => 1,
        'user' => '',
        'group' => '',
        'reusePort' => false,
        'eventLoop' => '', // Por padrão, vazio, escolhe automaticamente Select ou Event, sem ativar corrotinas
        'context' => [],
        'constructor' => [
            'requestClass' => Request::class,
            'logger' => Log::channel('default'),
            'appPath' => app_path(),
            'publicPath' => public_path()
        ]
    ],
    'minha-corrotina' => [
        'handler' => Http::class,
        'listen' => 'http://0.0.0.0:8686',
        'count' => 1,
        'user' => '',
        'group' => '',
        'reusePort' => false,
        // Ativar corrotinas requer configuração como Workerman\Events\Swoole::class ou Workerman\Events\Swow::class ou Workerman\Events\Fiber::class
        'eventLoop' => Workerman\Events\Swoole::class,
        'context' => [],
        'constructor' => [
            'requestClass' => Request::class,
            'logger' => Log::channel('default'),
            'appPath' => app_path(),
            'publicPath' => public_path()
        ]
    ]

    // ... outras configurações omitidas ...
];

Dica
O Webman pode configurar diferentes eventLoop para diferentes processos, o que significa que você pode ativar corrotinas seletivamente para processos específicos.
Por exemplo, no exemplo acima, o serviço na porta 8787 não tem corrotinas ativadas, enquanto o serviço na porta 8686 tem corrotinas ativadas. Com a configuração de proxy no nginx, é possível realizar uma implantação mista de corrotinas e não corrotinas.

Exemplo de corrotina

<?php
namespace app\controller;

use support\Response;
use Workerman\Coroutine;
use Workerman\Timer;

class IndexController
{
    public function index(): Response
    {
        Coroutine::create(function(){
            Timer::sleep(1.5);
            echo "hello coroutine\n";
        });
        return response('hello webman');
    }

}

Quando o eventLoop é Swoole, Swow ou Fiber, o Webman cria uma corrotina para cada requisição para ser executada, permitindo a criação de novas corrotinas enquanto processa a requisição.

Limitações de corrotinas

  • Ao usar Swoole ou Swow como drivers, as corrotinas mudarão automaticamente em caso de IO bloqueante, permitindo que o código síncrono seja executado de forma assíncrona.
  • Ao usar o driver Fiber, em caso de IO bloqueante, as corrotinas não mudarão, e o processo entrará em estado bloqueado.
  • Ao usar corrotinas, não é permitido que várias corrotinas operem simultaneamente sobre o mesmo recurso, como conexões de banco de dados ou operações de arquivos, pois isso pode gerar concorrência de recursos. O uso correto é empregar um pool de conexões ou bloqueios para proteger os recursos.
  • Ao usar corrotinas, não deve-se armazenar dados de estado relacionados à requisição em variáveis globais ou estáticas, pois isso pode provocar poluição de dados globais. O uso correto é utilizar o contexto de corrotina context para armazená-los.

Outras considerações

O Swow fará automaticamente hook em funções bloqueantes do PHP, mas como esse hook afeta certos comportamentos padrão do PHP, pode haver bugs caso você não utilize Swow, mas tenha o instalado.

Portanto, recomenda-se:

  • Se seu projeto não utiliza Swow, não instale a extensão Swow.
  • Se seu projeto utiliza Swow, configure eventLoop como Workerman\Events\Swow::class.

Contexto de corrotina

O ambiente de corrotina proíbe o armazenamento de informações de estado relacionadas à requisição em variáveis globais ou estáticas, pois isso pode causar poluição de variáveis globais, como em:

<?php

namespace app\controller;

use support\Request;
use Workerman\Timer;

class TestController
{
    protected static $name = '';

    public function index(Request $request)
    {
        static::$name = $request->get('name');
        Timer::sleep(5);
        return static::$name;
    }
}

Atenção
No ambiente de corrotina, não é proibido usar variáveis globais ou estáticas, mas sim proíbe-se usar variáveis globais ou estáticas para armazenar dados de estado relacionados à requisição.
Por exemplo, dados de configuração global, conexões de banco de dados, instâncias singleton de certas classes e outros dados de objeto que precisam ser compartilhados globalmente são recomendados a serem armazenados em variáveis globais ou estáticas.

Ao definir o número de processos como 1, quando fazemos duas requisições consecutivas:
http://127.0.0.1:8787/test?name=lilei
http://127.0.0.1:8787/test?name=hanmeimei
Esperamos que os resultados retornados pelas duas requisições sejam lilei e hanmeimei, respectivamente, mas na verdade ambos retornaram hanmeimei.
Isso ocorre porque a segunda requisição sobrescreveu a variável estática $name, e quando a primeira requisição termina de dormir e retorna, a variável estática $name já se tornou hanmeimei.

O método correto deve usar o context para armazenar os dados de estado da requisição

<?php

namespace app\controller;

use support\Request;
use support\Context;
use Workerman\Timer;

class TestController
{
    public function index(Request $request)
    {
        Context::set('name', $request->get('name'));
        Timer::sleep(5);
        return Context::get('name');
    }
}

A classe support\Context é utilizada para armazenar dados do contexto de corrotina, e quando a corrotina termina de ser executada, os dados do respectivo contexto são automaticamente deletados.
No ambiente de corrotinas, como cada requisição é uma corrotina isolada, os dados do contexto serão automaticamente destruídos quando a requisição for concluída.
No ambiente sem corrotinas, o contexto será automaticamente destruído ao fim da requisição.

Variáveis locais não causarão poluição de dados

<?php

namespace app\controller;

use support\Request;
use support\Context;
use Workerman\Timer;

class TestController
{
    public function index(Request $request)
    {
        $name = $request->get('name');
        Timer::sleep(5);
        return $name;
    }
}

Como $name é uma variável local, as corrotinas não podem acessar mutuamente as variáveis locais, portanto, usar variáveis locais é seguro em corrotinas.

Locker - Bloqueio

Às vezes, alguns componentes ou processos de negócios não consideram o ambiente de corrotinas, o que pode resultar em concorrência de recursos ou problemas de atomicidade. Nesse caso, pode-se usar Workerman\Locker para implementar bloqueios para o tratamento em fila, evitando problemas de concorrência.

<?php

namespace app\controller;

use Redis;
use support\Response;
use Workerman\Coroutine\Locker;

class IndexController
{
    public function index(): Response
    {
        static $redis;
        if (!$redis) {
            $redis = new Redis();
            $redis->connect('127.0.0.1', 6379);
        }
        // Se não bloquear, em Swoole pode ocorrer um erro como "Socket#10 já foi vinculado a outra corrotina#10"
        // Em Swow pode ocorrer coredump
        // Em Fiber, devido à extensão Redis ser IO bloqueante síncrono, não haverá problemas
        Locker::lock('redis');
        $time = $redis->time();
        Locker::unlock('redis');
        return json($time);
    }

}

Execução paralela

Quando precisamos executar várias tarefas em paralelo e obter resultados, podemos usar Workerman\Parallel para implementar.

<?php

namespace app\controller;

use support\Response;
use Workerman\Coroutine\Parallel;

class IndexController
{
    public function index(): Response
    {
        $parallel = new Parallel();
        for ($i=1; $i<5; $i++) {
            $parallel->add(function () use ($i) {
                // Fazer algo
                return $i;
            });
        }
        $results = $parallel->wait();
        return json($results); // Resposta: [1,2,3,4]
    }

}

Pool - Pool de Conexões

Corrotinas compartilhando a mesma conexão podem levar a confusões de dados, portanto, é necessário usar pools de conexões para gerenciar recursos de conexões de banco de dados, Redis, etc.

O Webman já fornece componentes como webman/database, webman/redis, webman/cache, webman/think-orm e webman/think-cache, que já incluem pools de conexões e suportam o uso em ambientes de corrotinas e não corrotinas.

Se você quiser adaptar um componente que não tenha pool de conexões, pode usar Workerman\Pool para implementá-lo, seguindo o código abaixo.

Componente de banco de dados

<?php
namespace app;

use Workerman\Coroutine\Context;
use Workerman\Coroutine;
use Workerman\Coroutine\Pool;

class Db
{
    private static ?Pool $pool = null;

    public static function __callStatic($name, $arguments)
    {
        if (self::$pool === null) {
            self::initializePool();
        }
        // Obter a conexão do contexto de corrotina, garantindo que a mesma corrotina use a mesma conexão
        $pdo = Context::get('pdo');
        if (!$pdo) {
            // Obter a conexão do pool de conexões
            $pdo = self::$pool->get();
            Context::set('pdo', $pdo);
            // Quando a corrotina terminar, a conexão será automaticamente devolvida
            Coroutine::defer(function () use ($pdo) {
                self::$pool->put($pdo);
            });
        }
        return call_user_func_array([$pdo, $name], $arguments);
    }

    private static function initializePool(): void
    {
        // Criar um pool de conexões, com um número máximo de conexões de 10
        self::$pool = new Pool(10);
        // Definir o criador de conexões (para simplificar, a leitura do arquivo de configuração é omitida)
        self::$pool->setConnectionCreator(function () {
            return new \PDO('mysql:host=127.0.0.1;dbname=your_database', 'your_username', 'your_password');
        });
        // Definir o fechador de conexões
        self::$pool->setConnectionCloser(function ($pdo) {
            $pdo = null;
        });
        // Definir verificador de heartbeat
        self::$pool->setHeartbeatChecker(function ($pdo) {
            $pdo->query('SELECT 1');
        });
    }

}

Uso

<?php
namespace app\controller;

use support\Response;
use app\Db;

class IndexController
{
    public function index(): Response
    {
        $value = Db::query('SELECT NOW() as now')->fetchAll();
        return json($value); // [{"now":"2025-02-06 23:41:03","0":"2025-02-06 23:41:03"}]
    }

}

Mais informações sobre corrotinas e componentes relacionados

Consulte a documentação de corrotinas do workerman

Implantação mista de corrotinas e não corrotinas

O Webman suporta a implantação mista de corrotinas e não corrotinas, como tratar negócios normais sem corrotinas e usar corrotinas para negócios de IO lento, encaminhando requisições para diferentes serviços através do nginx.

Por exemplo, no config/process.php

return [
    'webman' => [
        'handler' => Http::class,
        'listen' => 'http://0.0.0.0:8787',
        'count' => 1,
        'user' => '',
        'group' => '',
        'reusePort' => false,
        'eventLoop' => '', // Por padrão, vazio, escolhe automaticamente Select ou Event, sem ativar corrotinas
        'context' => [],
        'constructor' => [
            'requestClass' => Request::class,
            'logger' => Log::channel('default'),
            'appPath' => app_path(),
            'publicPath' => public_path()
        ]
    ],
    'minha-corrotina' => [
        'handler' => Http::class,
        'listen' => 'http://0.0.0.0:8686',
        'count' => 1,
        'user' => '',
        'group' => '',
        'reusePort' => false,
        // Ativar corrotinas requer configuração como Workerman\Events\Swoole::class ou Workerman\Events\Swow::class ou Workerman\Events\Fiber::class
        'eventLoop' => Workerman\Events\Swoole::class,
        'context' => [],
        'constructor' => [
            'requestClass' => Request::class,
            'logger' => Log::channel('default'),
            'appPath' => app_path(),
            'publicPath' => public_path()
        ]
    ],

    // ... outras configurações omitidas ...
];

Em seguida, configure o nginx para encaminhar requisições para diferentes serviços.

upstream webman {
    server 127.0.0.1:8787;
    keepalive 10240;
}

# Novo upstream para 8686
upstream task {
   server 127.0.0.1:8686;
   keepalive 10240;
}

server {
  server_name webman.com;
  listen 80;
  access_log off;
  root /path/webman/public;

  # Requisições que começam com /tast vão pela porta 8686, ajuste /tast para o prefixo que você precisa
  location /tast {
      proxy_set_header X-Real-IP $remote_addr;
      proxy_set_header Host $host;
      proxy_http_version 1.1;
      proxy_set_header Connection "";
      proxy_pass http://task;
  }

  # Outras requisições vão pela porta 8787
  location / {
      proxy_set_header X-Real-IP $remote_addr;
      proxy_set_header Host $host;
      proxy_http_version 1.1;
      proxy_set_header Connection "";
      if (!-f $request_filename){
          proxy_pass http://webman;
      }
  }
}