Multi-worker
(PHP 8.6+, true_async_server 0.6+)
TrueAsync Server tourne par défaut en mode mono-thread : une event-loop, un thread, tout le pipeline (accept → parse → dispatch → respond) sur un seul CPU. C'est le modèle le plus rapide pour des charges IO-bound typiques, mais il ne monte pas en charge sur les cœurs.
setWorkers(N) lève un pool intégré de N threads OS via Async\ThreadPool. Chaque worker re-bind les mêmes listeners, le noyau (Linux/BSD) répartit l'accept via SO_REUSEPORT. Chaque worker possède son propre event-loop indépendante, son propre opcache, ses propres pools de connexions.
Exemple de 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(); // bloque jusqu'à la fin de tous les workersHttpServer::start() dans le parent :
- Spawn un
Async\ThreadPoolde la taille demandée. - Copie la config + l'ensemble des handlers dans chaque worker via
transfer_obj. - Dans le worker, lance l'event-loop qui re-bind les listeners.
- Le parent fait
awaitsur la fin de tous les workers.
Le stop() cross-thread est encore dans la roadmap ; l'arrêt fonctionne via SIGINT/SIGTERM ou par épuisement normal du travail.
Bootloader
L'initialisation lourde d'un worker (autoload, warmup des pools, JIT-warmup) doit s'exécuter une seule fois au démarrage, pas à chaque requête. Pour cela il y a setBootloader(?\Closure $cb) :
$config
->setWorkers(4)
->setBootloader(function () {
// exécuté dans chaque worker une seule fois avant la task-loop
require __DIR__ . '/vendor/autoload.php';
// warmup du pool de connexions
Database::initPool(min: 4, max: 16);
// précompilation des routes critiques
Router::compile();
});La closure est deep-copy'ée une fois et exécutée sur chaque worker avant qu'il ne commence à prendre des tâches. Une exception levée dans le bootloader fait échouer tout le pool : le worker ne démarre pas.
S'applique uniquement quand setWorkers() > 1. null retire le bootloader.
Nécessite TrueAsync ABI v0.15+. Test :
server/core/021-bootloader.phpt.
Per-request scope
Depuis 0.6.5 chaque handler-coroutine s'exécute dans son propre scope, enfant du scope serveur. Cela donne deux sémantiques importantes :
Async\request_context(): contexte commun pour toute l'arborescence de coroutines de la requête (handler etspawnenfants).Async\current_context()reste per-coroutine.
use function Async\spawn;
use function Async\await;
use function Async\request_context;
$server->addHttpHandler(function ($req, $res) {
// Le contexte est vu par toute la branche de coroutines de la requête
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()), // request_id visible ici
spawn(fn() => fetchPosts()), // et ici
]));
$res->json(['user' => $user, 'posts' => $posts]);
});À comparer : current_context() crée des valeurs visibles uniquement dans la coroutine courante ; request_context() fournit un sous-arbre commun lié au scope de la requête.
SO_REUSEPORT et balancing
Sur Linux/BSD, le noyau répartit uniformément (mais non déterministiquement) les connexions entrantes entre tous les sockets ouverts avec SO_REUSEPORT sur le même (host, port). Chaque worker ouvre le sien ; aucun load balancer userspace n'est nécessaire, aucun verrou.
Sur Windows, l'équivalent de SO_REUSEPORT est moins prévisible ; déplacez le balancing plus haut (LB) ou utilisez single-worker + N processus avec des ports différents.
Transfert cross-thread des handlers
Si la configuration est montée dans un thread et que le serveur démarre dans un autre, HttpServer supporte le transfert. Depuis 0.2.0, le chemin de transfert préserve correctement les masks de protocoles (bug "silently dropped every request" corrigé ; voir CHANGELOG core/007-server-transfer-handler-dispatch.phpt).
Débogage du mode multi-thread
Le logging bruyant sur un exit inattendu de worker est ajouté en 0.6.3. Les exceptions $server->start() non capturées et les clean returns alors que l'await-loop attend encore les workers sont désormais visibles dans stderr (auparavant chaque cas faisait silencieusement chuter 1/N de la capacité d'accept sans signal pour l'opérateur).
Activez le logging INFO :
$config
->setLogSeverity(\TrueAsync\LogSeverity::INFO)
->setLogStream(STDERR);Combien de workers ?
Règle du pouce :
- IO-bound (web standard avec BD/HTTP) : commencer à
available_parallelism(), regarder l'utilisation CPU. - CPU-bound (rendering, compression lourde, gros JSON) :
available_parallelism()ou moins, regarder la p99 latency. - Mixte : un overcommit d'1 ou 2 workers (
N+1ouN+2) donne souvent une meilleure utilisation des cœurs sur les IO-stall.
$config->setWorkers(\Async\available_parallelism());
Async\available_parallelism()retourne le nombre de CPU disponibles pour le processus (prend en compte les quotas cgroup et l'affinity). Backed byuv_available_parallelismavec fallback suruv_cpu_info.