Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Concurrence

Dix mille connexions qui dorment, cent appels sortants Ă  lancer d’un coup, un calcul qui devrait occuper tous les cƓurs : un jour ou l’autre, vos requĂȘtes cessent d’ĂȘtre indĂ©pendantes les unes des autres, et c’est lĂ  que commence votre troisiĂšme question. PHP n’a ni threads dans sa version standard, ni boucle d’évĂ©nements intĂ©grĂ©e. La concurrence lui vient des processus, de bibliothĂšques bĂąties sur les coroutines que le langage a ajoutĂ©es en 8.1, et de runtimes qui apportent leur propre boucle d’évĂ©nements. Chacune de ces trois rĂ©ponses convient Ă  certaines charges et Ă©choue sur d’autres, et c’est cette diffĂ©rence qu’il faut connaĂźtre avant de lui confier un service.

Beaucoup de requĂȘtes

Un worker qui attend 40 millisecondes une rĂ©ponse de la base occupe un processus pendant 40 millisecondes, et aucune autre requĂȘte ne s’en aperçoit. Tout le modĂšle tient lĂ , pour le cas ordinaire des requĂȘtes HTTP indĂ©pendantes : le pool de processus du chapitre Le runtime, oĂč chaque worker bloque sur sa requĂȘte SQL, sa lecture de cache ou son appel HTTP sortant pendant que le systĂšme d’exploitation fait tourner les autres. Un runtime Ă  boucle d’évĂ©nements comme Node.js a besoin de code asynchrone pour qu’un appel lent ne gĂšle pas toutes les connexions. PHP a prĂ©fĂ©rĂ© donner un processus Ă  chaque connexion, et le code reste synchrone.

Cette simplicitĂ© se paie en mĂ©moire, connexion par connexion. Un worker qui garde un WebSocket ouvert, qui attend la fin d’un long poll ou qui diffuse une rĂ©ponse pendant une minute immobilise un processus entier tout ce temps, et un pool de quelques centaines de processus ne tiendra jamais dix mille connexions inactives. Pour cette charge-lĂ , le modĂšle par dĂ©faut de PHP est le mauvais.

Un pĂ©age d'autoroute vu de dessus. À gauche, une rangĂ©e de cabines, un Ă©lĂ©phant dans chacune, une voiture Ă  chaque cabine ; la file derriĂšre chaque cabine est courte et ordonnĂ©e. À droite, une seule voie avec une barriĂšre automatique oĂč une longue file de voitures passe sans s'arrĂȘter, un Ă©lĂ©phant surveillant un pupitre de contrĂŽle. Les deux cĂŽtĂ©s font passer le mĂȘme nombre de voitures

Beaucoup de connexions

Pour les charges qui exigent de nombreuses connexions simultanĂ©es dans un seul processus, PHP dispose de runtimes asynchrones, et ce sont des bibliothĂšques ou des extensions, pas des fonctionnalitĂ©s du langage. Par ordre alphabĂ©tique : AMPHP, un ensemble de bibliothĂšques non bloquantes construites sur une boucle d’évĂ©nements et sur les fibers du langage ; ReactPHP, un ensemble de bibliothĂšques bas niveau pilotĂ©es par les Ă©vĂ©nements, avec sa propre boucle, son serveur HTTP et ses composants de flux ; Swoole enfin, ou son fork OpenSwoole, une extension en C qui donne Ă  PHP une boucle d’évĂ©nements, des coroutines et un serveur HTTP et WebSocket intĂ©grĂ©, avec des appels d’entrĂ©es-sorties qui rendent la main au lieu de bloquer. FrankenPHP et RoadRunner, les runtimes worker du chapitre Le runtime, ne figurent pas dans cette liste parce qu’ils rĂ©pondent Ă  un autre besoin : ils gardent l’application chargĂ©e entre deux requĂȘtes, toujours une requĂȘte Ă  la fois par worker, et ne rendent pas le code concurrent.

Sous les deux premiers se trouve la Fiber, arrivĂ©e en PHP 8.1 : une fonction capable de se suspendre elle-mĂȘme, et que celui qui la tient peut reprendre plus tard. La RFC qui l’a introduite dit trĂšs prĂ©cisĂ©ment ce qu’elle n’est pas : « toutes les fibers vivent dans un seul thread, une seule fiber s’exĂ©cute Ă  la fois », et « le code bloquant, comme file_get_contents(), continuera de bloquer le processus entier, mĂȘme si d’autres fibers existent ». Le langage ne fournit aucun ordonnanceur. C’est une bibliothĂšque qui l’apporte, et chaque appel d’entrĂ©e-sortie du programme doit passer par elle pour rendre la main. Une application PHP asynchrone s’écrit donc contre le client HTTP, le client de base de donnĂ©es et les fonctions de fichiers d’AMPHP ou de ReactPHP, et non contre les fonctions standard. Une seule bibliothĂšque synchrone glissĂ©e par mĂ©garde dans les dĂ©pendances bloque toute la boucle.

Jusqu’oĂč une boucle d’évĂ©nements porte PHP, le test plaintext du benchmark multi-langages le montre : il ne fait aucun travail applicatif et mesure seulement combien de connexions en pipeline une couche HTTP sait servir.

TechEmpower Round 23, test plaintext, milliers de requĂȘtes par seconde Barres horizontales, sĂ©lection d'entrĂ©es : may-minihttp (Rust) 27 906, asp.net core (C#) 11 509, swoole (PHP) 3 519, micronaut (Java) 2 884, gin (Go) 1 640, nodejs 1 460, spring (Java) 833, fiber (Go) 646, php sur PHP-FPM 450, fastapi (Python) 338, php sur FrankenPHP 312, express (Node.js) 280, django (Python) 180, rails (Ruby) 139, laravel sur PHP-FPM 27 TechEmpower Round 23, test plaintext, milliers de requĂȘtes par seconde may-minihttp (Rust) 27 906 asp.net core (C#) 11 509 swoole (PHP) 3 519 micronaut (Java) 2 884 gin (Go) 1 640 nodejs 1 460 spring (Java) 833 fiber (Go) 646 php, PHP-FPM + nginx 450 fastapi (Python) 338 php, FrankenPHP 312 express (Node.js) 280 django (Python) 180 rails (Ruby) 139 laravel, PHP-FPM 27 TechEmpower Framework Benchmarks, Round 23 (24 fĂ©vrier 2025), test plaintext avec pipelining HTTP, jusqu'Ă  16 384 connexions simultanĂ©es. SĂ©lection de 15 entrĂ©es sur 514. Plaintext mesure la couche HTTP et la gestion des connexions, sans travail applicatif. En bleu : entrĂ©es PHP.

L’entrĂ©e Swoole sert 3,5 millions de requĂȘtes par seconde, devant Micronaut, Gin, Node.js et Spring, quand PHP nu sous PHP-FPM en sert 450 000 et Laravel sous PHP-FPM 27 000. Le plafond est ailleurs : ASP.NET Core Ă  11,5 millions, et tout en haut un serveur Python adossĂ© Ă  du C et un serveur Rust, autour de 28 millions. Avec une boucle d’évĂ©nements, PHP joue dans le deuxiĂšme groupe, pas dans le premier. Sans, il joue dans le quatriĂšme.

Beaucoup de cƓurs

Le programme spectral-norm le plus rapide en PHP dans le Benchmarks Game termine en 18 secondes de temps rĂ©el pour 72 secondes de CPU rĂ©parties sur plusieurs cƓurs, contre 90 secondes pour le meilleur programme Python et 1,6 seconde pour Node.js. Cet Ă©cart entre temps CPU et temps rĂ©el, c’est Ă  cela que ressemble le calcul parallĂšle en PHP, et une fois de plus il vient des processus. L’interprĂ©teur en ligne de commande forke avec pcntl_fork(), lance des sous-processus avec proc_open() ou fait tourner plusieurs scripts sous un superviseur, et les rĂ©sultats reviennent par des tubes, des fichiers, une base ou une file d’attente. L’extension parallel propose bien des threads, mais sur la version thread-safe de l’interprĂ©teur, que peu de distributions livrent. Le parallĂ©lisme par fork fonctionne, et il fonctionne dans la classe de vitesse de l’interprĂ©teur dĂ©crite dans DĂ©bit et latence.

L’ordonnanceur entrera-t-il un jour dans le moteur ? La question est ouverte sur la liste internals. Une RFC intitulĂ©e « Concurrency Support in the PHP Engine », qui ajouterait un ordonnanceur et de la concurrence structurĂ©e au langage lui-mĂȘme, y Ă©tait en discussion au moment oĂč j’écris, sans vote, aprĂšs qu’une premiĂšre proposition sur le mĂȘme sujet a Ă©tĂ© abandonnĂ©e en juillet 2026. En septembre 2026, le langage a des fibers, et l’ordonnanceur est une bibliothĂšque.

Ce qui ne doit pas faire attendre l’utilisateur

La concurrence dont vous aurez besoin le plus souvent n’a rien de spectaculaire : c’est le travail que l’utilisateur ne devrait pas avoir Ă  attendre. Une requĂȘte a une durĂ©e limite et une rĂ©ponse Ă  rendre, donc tout ce qui prend plus de temps doit en sortir. L’idiome, c’est une file d’attente et un processus worker. La requĂȘte dĂ©pose un travail dans une table ou un courtier de messages, puis rĂ©pond. De son cĂŽtĂ©, un script PHP en ligne de commande, maintenu en vie par un superviseur, dĂ©pile la file et exĂ©cute les travaux un par un. Ce script n’a ni limite de temps ni limite de mĂ©moire tant que vous ne lui en donnez pas, alors un worker de production fixe les deux et s’arrĂȘte proprement aprĂšs quelques milliers de travaux, pour que le superviseur relance un processus neuf. La plupart des frameworks full-stack livrent ce schĂ©ma avec un pilote pour les courtiers habituels, et cron couvre les tĂąches planifiĂ©es.

La limite : la concurrence de PHP est Ă  gros grain. Des processus pour les requĂȘtes, des processus pour les cƓurs, une file d’attente pour le travail en arriĂšre-plan, et un runtime Ă  part, avec ses propres bibliothĂšques d’entrĂ©es-sorties, pour le cas des connexions nombreuses. Si votre produit est un service temps rĂ©el avec des dizaines de milliers de connexions ouvertes, vous passerez votre premier mois Ă  choisir puis Ă  apprendre l’un des runtimes asynchrones, et vous dĂ©couvrirez que la plupart des bibliothĂšques de l’écosystĂšme n’ont pas Ă©tĂ© Ă©crites pour lui. Si votre produit est fait de requĂȘtes et de rĂ©ponses, vous ne rencontrerez jamais la question.

Ce que vous pouvez vĂ©rifier vous-mĂȘme

Prenez le scĂ©nario Ă  connexions nombreuses que vous avez vraiment, si vous en avez un, et Ă©crivez-le deux fois dans l’aprĂšs-midi : une fois avec AMPHP ou ReactPHP, une fois avec Swoole ou OpenSwoole. Comptez ensuite combien des bibliothĂšques dont vous dĂ©pendriez offrent un client non bloquant. Si c’est la plupart, la voie asynchrone vous est ouverte. Si elles sont rares, la conclusion honnĂȘte est que PHP est le mauvais outil pour ce service-lĂ , et le reste de votre systĂšme est une dĂ©cision Ă  part.

Reste Ă  savoir si le langage lui-mĂȘme est de ceux que vous auriez envie d’écrire, et cette question-lĂ , vous y rĂ©pondez en le lisant.