Corrutinas
Webman está desarrollado sobre Workerman, por lo que Webman puede utilizar las características de corrutinas de Workerman.
Las corrutinas admiten tres controladores: Swoole
, Swow
y Fiber
.
Requisitos Previos
- PHP >= 8.1
- Workerman >= 5.1.0 (
composer require workerman/workerman ~v5.1
) - webman-framework >= 2.1 (
composer require workerman/webman-framework ~v2.1
) - Se debe instalar la extensión swoole o swow, o instalar
composer require revolt/event-loop
(Fiber) - Las corrutinas están desactivadas por defecto y es necesario configurar
eventLoop
para activarlas.
Método de Activación
Webman admite habilitar diferentes controladores para diferentes procesos, por lo que puedes configurar el controlador de corrutinas en config/process.php
a través de eventLoop
:
return [
'webman' => [
'handler' => Http::class,
'listen' => 'http://0.0.0.0:8787',
'count' => 1,
'user' => '',
'group' => '',
'reusePort' => false,
'eventLoop' => '', // Por defecto vacío, selecciona automáticamente Select o Event, no activa corrutinas
'context' => [],
'constructor' => [
'requestClass' => Request::class,
'logger' => Log::channel('default'),
'appPath' => app_path(),
'publicPath' => public_path()
]
],
'my-coroutine' => [
'handler' => Http::class,
'listen' => 'http://0.0.0.0:8686',
'count' => 1,
'user' => '',
'group' => '',
'reusePort' => false,
// Activar corrutinas requiere configurar como Workerman\Events\Swoole::class o Workerman\Events\Swow::class o Workerman\Events\Fiber::class
'eventLoop' => Workerman\Events\Swoole::class,
'context' => [],
'constructor' => [
'requestClass' => Request::class,
'logger' => Log::channel('default'),
'appPath' => app_path(),
'publicPath' => public_path()
]
]
// ... Otras configuraciones omitidas ...
];
Sugerencia
Webman puede configurar diferenteseventLoop
para diferentes procesos, lo que significa que puedes activar corrutinas selectivamente para procesos específicos.
Por ejemplo, en la configuración anterior, el servicio en el puerto 8787 no tiene activadas las corrutinas, mientras que el servicio en el puerto 8686 sí las tiene. Combinado con la redirección de nginx, se puede lograr un despliegue híbrido de corrutinas y no corrutinas.
Ejemplo de Corrutinas
<?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');
}
}
Cuando eventLoop
es Swoole
, Swow
o Fiber
, Webman crea una corrutina para cada solicitud, y se pueden seguir creando nuevas corrutinas para ejecutar el código de negocio mientras se procesa la solicitud.
Limitaciones de Corrutinas
- Cuando se usa Swoole o Swow como controlador, las corrutinas se cambian automáticamente cuando se encuentran con IO bloqueante, lo que permite ejecutar código sincrónico de manera asincrónica.
- Cuando se utiliza el controlador Fiber, si se encuentra con IO bloqueante, no ocurrirá cambio de corrutina, y el proceso entrará en estado bloqueante.
- Al usar corrutinas, no debe haber múltiples corrutinas operando simultáneamente sobre el mismo recurso, como conexiones de bases de datos, operaciones de archivos, etc., ya que esto puede causar competencia por recursos. La práctica adecuada es utilizar un pool de conexiones o un bloqueo para proteger los recursos.
- Al usar corrutinas, no se debe almacenar datos de estado relacionados con la solicitud en variables globales o estáticas, ya que esto puede causar contaminación de datos globales. La práctica adecuada es usar el
context
de la corrutina para almacenarlos y recuperarlos.
Otras Consideraciones
Swow a nivel inferior engancha automáticamente las funciones bloqueantes de PHP, pero debido a que esta acción afecta ciertos comportamientos predeterminados de PHP, podría haber errores si instalas Swow sin usarlo realmente.
Por lo tanto, se recomienda:
- Si tu proyecto no utiliza Swow, no instales la extensión Swow.
- Si tu proyecto utiliza Swow, configura
eventLoop
comoWorkerman\Events\Swow::class
.
Contexto de Corrutinas
En un entorno de corrutinas, está prohibido almacenar información de estado relacionada con la solicitud en variables globales o estáticas, ya que esto puede llevar a la contaminación de variables globales, por ejemplo:
<?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;
}
}
Nota
No está prohibido usar variables globales o estáticas en un entorno de corrutinas, sino que está prohibido usar variables globales o estáticas para almacenar datos de estado relacionados con la solicitud.
Por ejemplo, se recomienda usar variables globales o estáticas para almacenar datos de objetos compartidos globalmente, como configuraciones globales, conexiones de base de datos y algunas instancias singleton de clases.
Si se establece el número de procesos a 1, cuando realizamos dos solicitudes consecutivas:
http://127.0.0.1:8787/test?name=lilei
http://127.0.0.1:8787/test?name=hanmeimei
Esperamos que los resultados de ambas solicitudes sean respectivamente lilei
y hanmeimei
, pero en realidad ambas devuelven hanmeimei
.
Esto se debe a que la segunda solicitud sobrescribe la variable estática $name
, y cuando la primera solicitud finaliza su espera, vuelve con la variable estática $name
, que ya se ha convertido en hanmeimei
.
La forma correcta es usar el contexto para almacenar datos de estado de la solicitud
<?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');
}
}
La clase support\Context
se utiliza para almacenar datos de contexto de corrutinas, y cuando la corrutina termina de ejecutarse, los datos del contexto se eliminarán automáticamente.
En un entorno de corrutinas, dado que cada solicitud es una corrutina separada, los datos del contexto se destruyen automáticamente al finalizar la solicitud.
En un entorno sin corrutinas, el contexto también se destruye automáticamente al finalizar la solicitud.
Las variables locales no causan contaminación de datos
<?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;
}
}
Dado que $name
es una variable local, las corrutinas no pueden acceder mutuamente a las variables locales, por lo que usar variables locales es seguro en corrutinas.
Locker (Bloqueo)
A veces, algunos componentes o negocios no consideran el entorno de corrutinas, lo que puede generar problemas de competencia de recursos o atomicidad. En estos casos, puedes usar Workerman\Locker
para implementar un bloqueo y así manejar la cola, previniendo problemas de concurrencia.
<?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);
}
// Si no se bloquea, en Swoole se generará un error como "Socket#10 has already been bound to another coroutine#10"
// En Swow podría desencadenar un coredump
// En Fiber, debido a que la extensión Redis es IO bloqueante y sincrónica, no habrá problemas
Locker::lock('redis');
$time = $redis->time();
Locker::unlock('redis');
return json($time);
}
}
Ejecución Paralela
Cuando necesitamos ejecutar múltiples tareas en paralelo y obtener resultados, podemos usar Workerman\Parallel
para implementarlo.
<?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) {
// Hacer algo
return $i;
});
}
$results = $parallel->wait();
return json($results); // Respuesta: [1,2,3,4]
}
}
Pool (Pool de Conexiones)
El uso de múltiples corrutinas compartiendo la misma conexión puede provocar confusión de datos, por lo que es necesario usar un pool de conexiones para gestionar recursos de conexiones a bases de datos, redis, etc.
Webman ya proporciona componentes como webman/database, webman/redis, webman/cache, webman/think-orm y webman/think-cache, todos los cuales integran Pool de Conexiones y admiten su uso tanto en entornos de corrutinas como no corrutinas.
Si deseas adaptar un componente que no tiene Pool de Conexiones, puedes usar Workerman\Pool
para hacerlo, consulta el siguiente código.
Componente de Base de Datos
<?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();
}
// Obtener la conexión del contexto de la corrutina, asegurando que la misma corrutina use la misma conexión
$pdo = Context::get('pdo');
if (!$pdo) {
// Obtener conexión del pool de conexiones
$pdo = self::$pool->get();
Context::set('pdo', $pdo);
// Al finalizar la corrutina, automáticamente devolver la conexión
Coroutine::defer(function () use ($pdo) {
self::$pool->put($pdo);
});
}
return call_user_func_array([$pdo, $name], $arguments);
}
private static function initializePool(): void
{
// Crear un pool de conexiones con un máximo de 10 conexiones
self::$pool = new Pool(10);
// Configurar el creador de conexiones (por simplicidad, se omite la lectura del archivo de configuración)
self::$pool->setConnectionCreator(function () {
return new \PDO('mysql:host=127.0.0.1;dbname=your_database', 'your_username', 'your_password');
});
// Configurar el cerrador de conexiones
self::$pool->setConnectionCloser(function ($pdo) {
$pdo = null;
});
// Configurar el verificador de latido
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"}]
}
}
Más Información sobre Corrutinas y Componentes Relacionados
Consulta la documentación de corrutinas de Workerman.
Despliegue Híbrido de Corrutinas y No Corrutinas
Webman admite el despliegue híbrido de corrutinas y no corrutinas, por ejemplo, manejar negocios normales sin corrutinas y manejar negocios de IO lento con corrutinas, mediante la redirección de solicitudes a diferentes servicios usando nginx.
Por ejemplo en config/process.php
return [
'webman' => [
'handler' => Http::class,
'listen' => 'http://0.0.0.0:8787',
'count' => 1,
'user' => '',
'group' => '',
'reusePort' => false,
'eventLoop' => '', // Por defecto vacío, selecciona automáticamente Select o Event, no activa corrutinas
'context' => [],
'constructor' => [
'requestClass' => Request::class,
'logger' => Log::channel('default'),
'appPath' => app_path(),
'publicPath' => public_path()
]
],
'my-coroutine' => [
'handler' => Http::class,
'listen' => 'http://0.0.0.0:8686',
'count' => 1,
'user' => '',
'group' => '',
'reusePort' => false,
// Activar corrutinas requiere configurar como Workerman\Events\Swoole::class o Workerman\Events\Swow::class o Workerman\Events\Fiber::class
'eventLoop' => Workerman\Events\Swoole::class,
'context' => [],
'constructor' => [
'requestClass' => Request::class,
'logger' => Log::channel('default'),
'appPath' => app_path(),
'publicPath' => public_path()
]
],
// ... Otras configuraciones omitidas ...
];
Luego, configura nginx para redirigir solicitudes a diferentes servicios
upstream webman {
server 127.0.0.1:8787;
keepalive 10240;
}
# Nueva upstream para el puerto 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;
# Las solicitudes que comienzan con /tast van al puerto 8686, ajusta /tast según sea necesario
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;
}
# Otras solicitudes van al puerto original 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;
}
}
}