Multi-worker
(PHP 8.6+, true_async_server 0.6+)
TrueAsync Server funciona por defecto en modo single-threaded: un event-loop, un hilo, todo el pipeline (accept → parse → dispatch → respond) sobre un solo CPU. Es el modelo más rápido para cargas IO-bound típicas, pero no escala por núcleos.
setWorkers(N) levanta el pool integrado de N hilos del sistema operativo mediante Async\ThreadPool. Cada worker hace re-bind sobre los mismos listeners y el kernel (Linux/BSD) distribuye el accept mediante SO_REUSEPORT. Cada worker tiene su propio event-loop independiente, su propio opcache, sus propios pools de conexiones.
Ejemplo base
use TrueAsync\HttpServer;
use TrueAsync\HttpServerConfig;
$server = new HttpServer(
(new HttpServerConfig())
->addListener('0.0.0.0', 8080)
->setWorkers(4)
);
$server->addHttpHandler(function ($req, $res) {
$res->json(['pid' => getmypid(), 'tid' => /* TID */]);
});
$server->start(); // bloquea hasta que todos los workers terminenHttpServer::start() en el padre:
- Spawnea un
Async\ThreadPooldel tamaño requerido. - Copia el config + el conjunto de manejadores en cada worker mediante
transfer_obj. - Dentro del worker arranca el event-loop, que hace re-bind de los listeners.
- El padre hace
awaitdel final de todos los workers.
El stop() entre hilos está en la hoja de ruta; la parada actual funciona con SIGINT/SIGTERM o por agotamiento natural del trabajo.
Bootloader
La inicialización pesada del worker (autoload, calentamiento de pools, JIT-warmup) debe ejecutarse una sola vez al arrancar, no por cada solicitud. Para eso existe setBootloader(?\Closure $cb):
$config
->setWorkers(4)
->setBootloader(function () {
// se ejecuta una vez en cada worker antes del task-loop
require __DIR__ . '/vendor/autoload.php';
// calentamiento del pool de conexiones
Database::initPool(min: 4, max: 16);
// precompilación de rutas críticas
Router::compile();
});La closure se deep-copia una vez y se lanza en cada worker antes de que este empiece a aceptar tareas. Una excepción lanzada en el bootloader hace fallar al pool entero: el worker no arranca.
Solo se aplica con setWorkers() > 1. null elimina el bootloader.
Requiere TrueAsync ABI v0.15+. Test:
server/core/021-bootloader.phpt.
Scope por solicitud
Desde 0.6.5 cada corrutina-manejador se ejecuta en su propio scope, hijo del scope del servidor. Esto da dos semánticas importantes:
Async\request_context(): contexto común para todo el árbol de corrutinas de la solicitud (handler yspawnhijos).Async\current_context()sigue siendo per-coroutine.
use function Async\spawn;
use function Async\await;
use function Async\request_context;
$server->addHttpHandler(function ($req, $res) {
// El contexto lo ve toda la rama de corrutinas de la solicitud
request_context()->set('request_id', $req->getHeader('X-Request-Id') ?? bin2hex(random_bytes(8)));
request_context()->set('user_id', authUser($req));
// Fan-out
[$user, $posts] = await(\Async\all([
spawn(fn() => fetchUser()), // aquí ve request_id
spawn(fn() => fetchPosts()), // y aquí también
]));
$res->json(['user' => $user, 'posts' => $posts]);
});Comparativa: current_context() crea valores visibles solo en la corrutina actual; request_context() ofrece un subárbol común vinculado al scope de la solicitud.
SO_REUSEPORT y balanceo
En Linux/BSD el kernel distribuye de forma uniforme (pero no determinista) las conexiones entrantes entre todos los sockets abiertos con SO_REUSEPORT sobre el mismo (host, port). Cada worker abre el suyo; no hace falta un balanceador en userspace ni bloqueos.
En Windows el equivalente a SO_REUSEPORT es menos predecible; lleva el balanceo más arriba (LB) o usa single-worker + N procesos con puertos distintos.
Transferencia entre hilos de los manejadores
Si la configuración se prepara en un hilo y el servidor se arranca en otro, HttpServer admite transfer. Desde 0.2.0 la ruta de transfer mueve correctamente las máscaras de protocolo (corregido el bug "silently dropped every request"; véase el CHANGELOG core/007-server-transfer-handler-dispatch.phpt).
Depuración del modo multihilo
En 0.6.3 se añadió logging ruidoso ante una salida inesperada de un worker. Las excepciones no capturadas de $server->start() y los returns limpios mientras el bucle await todavía espera a los workers ahora aparecen en stderr (antes cada caso tiraba en silencio 1/N de la capacidad de accept sin avisar al operador).
Activa el logging INFO:
$config
->setLogSeverity(\TrueAsync\LogSeverity::INFO)
->setLogStream(STDERR);¿Cuántos workers?
Regla práctica:
- IO-bound (web estándar con BD/HTTP): empezar por
available_parallelism()y observar la utilización de CPU. - CPU-bound (renderizado, mucha compresión, JSON grandes):
available_parallelism()o menos, observando la latencia p99. - Mixto: sobre-suscribir en 1–2 workers (
N+1oN+2) suele dar mejor utilización de núcleos ante IO-stall.
$config->setWorkers(\Async\available_parallelism());
Async\available_parallelism()devuelve el número de CPUs disponibles para el proceso (tiene en cuenta la cuota de cgroup y la affinity). Respaldado poruv_available_parallelismcon fallback auv_cpu_info.