Multiplexage de Fibers

OxPHP utilise les Fibers de PHP pour traiter plusieurs requêtes HTTP simultanément sur un seul thread worker. Lorsqu'une requête appelle oxphp_sleep() ou oxphp_async_await() (lorsque le pool asynchrone est activé), elle se suspend et le thread worker prend immédiatement en charge la requête suivante. Un seul worker peut gérer des centaines de requêtes en cours sans threads supplémentaires.

Fonctionnement

L'ordonnanceur exécute de nombreuses requêtes sur un seul thread, en attribuant à chacune sa propre Fiber et en basculant de l'une à l'autre chaque fois qu'une Fiber se suspend.

  1. Une requête arrive et l'ordonnanceur l'assigne à une Fiber : un contexte d'exécution léger doté de sa propre pile et de son propre état PHP.
  2. La Fiber exécute le gestionnaire oxphp_worker(). Si le gestionnaire se termine sans se suspendre, la réponse est envoyée et la Fiber est recyclée sans surcoût par rapport à un worker à requête unique.
  3. Si le gestionnaire appelle une fonction suspensive (oxphp_sleep(), oxphp_usleep(), oxphp_async_await()), la Fiber rend la main à l'ordonnanceur.
  4. L'ordonnanceur prend en charge les nouvelles requêtes entrantes (en créant de nouvelles Fibers) et reprend les Fibers suspendues dont les conditions d'attente sont satisfaites (minuterie expirée, résultat asynchrone prêt).
  5. L'état PHP de chaque Fiber (superglobales, en-têtes de réponse, tampons de sortie, pile de la VM) est sauvegardé lors de la suspension et restauré à la reprise. Les Fibers sont totalement isolées les unes des autres.
graph LR
  W["Worker Thread"]
  W --> A0["Fiber A: handling /api/users"]
  A0 --> A1["oxphp_sleep(0.5)"]
  A1 --> A2["suspended"]
  A2 --> A3["resumed"]
  A3 --> A4["response"]
  W --> B0["Fiber B: handling /api/orders"]
  B0 --> B1["oxphp_async_await($p)"]
  B1 --> B2["suspended"]
  B2 --> B3["resumed"]
  B3 --> B4["response"]
  W --> C0["Fiber C: handling /health"]
  C0 --> C1["response (no suspension, zero overhead)"]

Configuration

Le multiplexage de Fibers s'active automatiquement lorsque le mode worker est activé. Aucune variable d'environnement supplémentaire n'est nécessaire.

Variable Par défaut Description
WORKER_MODE_ENABLED false Définissez sur true et faites pointer ENTRY_FILE vers un bootstrap .php pour activer le mode worker et le multiplexage de Fibers
PHP_WORKERS CPU / 2 (min 1) Nombre de threads worker. Chaque thread exécute son propre ordonnanceur indépendant, avec jusqu'à 256 Fibers concurrentes

Le nombre maximal de Fibers concurrentes par thread worker est de 256. Avec 4 threads worker, OxPHP peut traiter jusqu'à 1 024 requêtes en cours simultanément.

Points de suspension

Ces fonctions suspendent la Fiber courante et permettent à d'autres requêtes de s'exécuter sur le même thread :

Fonction Ce qui se passe
oxphp_sleep(float $seconds) Suspend la Fiber pendant la durée indiquée. Les autres Fibers continuent de s'exécuter
oxphp_usleep(int $microseconds) Identique à oxphp_sleep() avec une granularité à la microseconde (minimum 1 ms)
oxphp_async_await(int $promise_id) Suspend la Fiber jusqu'à ce que la tâche asynchrone se termine sur le pool de threads en arrière-plan

Ces fonctions ne suspendent pas la Fiber :

Fonction Comportement
oxphp_stream_flush() Envoie immédiatement un fragment au client et retourne. À utiliser avec oxphp_sleep() dans les boucles SSE
oxphp_finish_request() Envoie la réponse complète et poursuit l'exécution PHP. Ne rend pas la main
Note

Les fonctions natives sleep() et usleep() de PHP bloquent l'intégralité du thread worker. Utilisez toujours oxphp_sleep() et oxphp_usleep() pour obtenir un comportement coopératif.

Exemples PHP

Traitement concurrent de base

Les requêtes qui ne se suspendent pas s'exécutent à pleine vitesse, sans surcoût lié aux Fibers :

worker.php
<?php oxphp_worker(function () { $path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH); if ($path === '/health') { echo json_encode(['status' => 'ok']); return; // No suspension — runs at full speed } if ($path === '/slow') { oxphp_sleep(2.0); // Yields for 2 seconds — other requests run echo "Done after 2s delay"; return; } echo "Hello"; });

Appels d'API non bloquants

Combinez oxphp_async() et oxphp_async_await() pour effectuer des appels d'API externes sans bloquer le worker :

worker.php
<?php oxphp_worker(function () { // Dispatch two API calls to the async thread pool $p1 = oxphp_async(fn() => file_get_contents('https://api.example.com/users')); $p2 = oxphp_async(fn() => file_get_contents('https://api.example.com/orders')); // Await both — the fiber suspends, other requests run on this thread $users = oxphp_async_await($p1); $orders = oxphp_async_await($p2); header('Content-Type: application/json'); echo json_encode(['users' => json_decode($users), 'orders' => json_decode($orders)]); });

SSE avec sommeil coopératif

Entrelacez les Server-Sent Events avec d'autres requêtes à l'aide de oxphp_stream_flush() et oxphp_sleep() :

worker.php
<?php oxphp_worker(function () { header('Content-Type: text/event-stream'); header('Cache-Control: no-cache'); for ($i = 0; $i < 30; $i++) { echo "data: " . json_encode(['count' => $i, 'time' => time()]) . "\n\n"; oxphp_stream_flush(); // Send chunk now (does not suspend) oxphp_sleep(1.0); // Yield for 1 second (other requests run) } });

E/S bloquantes

Le multiplexage de Fibers est coopératif, pas préemptif. Une Fiber qui appelle une fonction bloquante gèle l'intégralité du thread worker : aucune autre Fiber de ce thread ne peut progresser.

Fonctions qui bloquent le worker

  • file_get_contents(), fopen(), fread()
  • curl_exec(), curl_multi_exec()
  • Les requêtes PDO, mysqli_query()
  • sleep(), usleep() de PHP (utilisez plutôt oxphp_sleep())
  • La résolution DNS (gethostbyname())
  • Toute E/S réseau ou disque synchrone

Comment éviter le blocage

Enveloppez les opérations bloquantes dans oxphp_async() pour les exécuter sur le pool de threads asynchrone :

php
<?php // WRONG — blocks the entire worker thread $html = file_get_contents('https://example.com'); // CORRECT — runs on async pool, fiber yields $promise = oxphp_async(fn() => file_get_contents('https://example.com')); $html = oxphp_async_await($promise);

Pour les requêtes de base de données :

php
<?php $db = new PDO('mysql:host=db;dbname=app', 'root', 'secret'); // WRONG — blocks the worker $users = $db->query('SELECT * FROM users WHERE active = 1')->fetchAll(); // CORRECT — query runs on async thread, fiber yields $promise = oxphp_async(function () { $db = new PDO('mysql:host=db;dbname=app', 'root', 'secret'); return $db->query('SELECT * FROM users WHERE active = 1')->fetchAll(); }); $users = oxphp_async_await($promise);
Note

Les connexions de base de données ne peuvent pas être passées à oxphp_async(), car les objets ne sont pas sérialisables entre les threads. Créez la connexion à l'intérieur de la closure asynchrone, ou effectuez la requête directement dans la Fiber si celle-ci est suffisamment rapide pour que le blocage soit acceptable.

Warning

oxphp_async() nécessite ASYNC_WORKERS > 0. Lorsque le pool asynchrone est désactivé (par défaut), l'appel à oxphp_async() lève OxPHP\Async\AsyncException.

Comment les Fibers sont recyclées

Les piles C des Fibers sont allouées une seule fois et réutilisées d'une requête à l'autre. Lorsqu'une Fiber termine le traitement d'une requête, elle n'est pas détruite ; elle se suspend plutôt et rend la main à l'ordonnanceur, puis elle est ajoutée à une liste libre. La requête suivante réutilise la pile C existante, ce qui évite une allocation mémoire coûteuse.

Les piles de la VM PHP (utilisées pour les cadres d'appel de fonction) sont allouées à neuf pour chaque requête et libérées lorsque le gestionnaire retourne.

Exemple Docker

compose.yaml
services: app: image: ghcr.io/oxphp/oxphp:0.10.0 ports: - "80:80" environment: - DOCUMENT_ROOT=/var/www/html/public - WORKER_MODE_ENABLED=true - ENTRY_FILE=worker.php - PHP_WORKERS=4 - ASYNC_WORKERS=8

Avec cette configuration, chacun des 4 threads worker peut gérer jusqu'à 256 Fibers concurrentes, et les E/S bloquantes sont déchargées vers 8 threads worker asynchrones.

Dépannage

Les requêtes deviennent lentes lorsqu'une requête effectue de lourdes E/S

Une Fiber appelle une fonction bloquante (requête de base de données, requête HTTP, lecture de fichier) sans oxphp_async(). Cela bloque l'intégralité du thread worker.

Correctif : Enveloppez les appels bloquants dans oxphp_async() :

php
<?php $promise = oxphp_async(fn() => file_get_contents($url)); $result = oxphp_async_await($promise);
"Async pool is disabled. Set ASYNC_WORKERS > 0 to enable."

Le pool asynchrone n'est pas configuré. Lorsque ASYNC_WORKERS=0 (par défaut), toutes les fonctions asynchrones lèvent OxPHP\Async\AsyncException.

Correctif : Définissez ASYNC_WORKERS sur une valeur positive :

bash
ASYNC_WORKERS=8
"Failed to dispatch async task" lors de l'utilisation de oxphp_async()

Le pool asynchrone fonctionne, mais il a atteint sa capacité maximale.

Correctif : Augmentez ASYNC_WORKERS ou ASYNC_QUEUE_CAPACITY :

bash
ASYNC_WORKERS=8 ASYNC_QUEUE_CAPACITY=512
oxphp_sleep() ne rend pas la main aux autres requêtes

Le multiplexage de Fibers ne fonctionne qu'en mode worker. En mode traditionnel, oxphp_sleep() se rabat sur un usleep() bloquant.

Correctif : Activez le mode worker en définissant WORKER_MODE_ENABLED=true.

Consommation mémoire élevée avec de nombreuses requêtes concurrentes

Chaque Fiber utilise une pile C (8 MiB par défaut, configurée via le paramètre ini fiber.stack_size de PHP) ainsi qu'une pile de la VM PHP par requête. Avec 256 Fibers concurrentes, la consommation de pile C dans le pire des cas atteint 2 GiB par thread worker.

Correctif : Réduisez fiber.stack_size dans php.ini si votre application n'utilise pas de récursion profonde :

php.ini
fiber.stack_size = 512K

Limitations

  • Mode worker uniquement — le multiplexage de Fibers n'est pas disponible en mode traditionnel
  • 256 Fibers par worker — limite stricte, non configurable à l'exécution
  • Coopératif uniquement — le code gourmand en CPU (boucles serrées, calculs lourds) prive les autres Fibers de ressources. Il n'y a pas de préemption
  • Les E/S bloquantes bloquent le thread — tous les appels bloquants doivent être enveloppés dans oxphp_async() pour une véritable concurrence
  • Les fonctions natives sleep()/usleep() de PHP ne prennent pas en charge les Fibers — utilisez oxphp_sleep()/oxphp_usleep()
  • oxphp_async_await_race() et oxphp_async_await_any() ne rendent pas la main — elles bloquent actuellement même à l'intérieur d'une Fiber. oxphp_async_await_all() suspend bel et bien la Fiber pendant l'attente, elle est donc compatible avec les Fibers ; pour race/any, utilisez des appels séquentiels à oxphp_async_await() si vous avez besoin que le thread reste coopératif

Voir aussi

  • Mode worker — processus PHP persistants et l'API oxphp_worker()
  • Promesses asynchrones — pool de threads en arrière-plan pour décharger les E/S bloquantes
  • SSE — streaming en temps réel combiné à un sommeil coopératif basé sur les Fibers
  • Fonctions PHPoxphp_sleep(), oxphp_usleep() et d'autres fonctions compatibles avec les Fibers
  • Référence de configurationWORKER_MODE_ENABLED, ENTRY_FILE, PHP_WORKERS, ASYNC_WORKERS