L'RFC di PHP che definisce una Polling API nativa è stata approvata e unita al branch master del linguaggio, offrendo agli sviluppatori un modo veloce, a livello di sistema operativo, per eseguire il polling dei file descriptor. Insieme alle Fibers aggiunte di recente, PHP dispone ora di una base integrata ed efficiente per il codice asincrono, colmando una lacuna che per lungo tempo ha costretto le librerie a improvvisare le proprie soluzioni alternative.

Fibers ed event loop, separati

Le Fibers sono una primitiva di controllo del flusso a basso livello. Consentono a una funzione di sospendere la propria esecuzione in un punto scelto e di riprenderla successivamente esattamente da dove si era interrotta. Fondamentalmente, una fiber non sa nulla di socket, timer o di qualsiasi altra sorgente di I/O; si limita a mettersi in pausa e a riavviarsi su richiesta.

Un event loop è lo scheduler che decide quando una fiber in pausa debba essere ripresa. In un tipico stack asincrono, il loop monitora un set di file descriptor, attende che diventino leggibili o scrivibili e poi risveglia la fiber appropriata.

In precedenza PHP disponeva delle Fibers ma mancava di un meccanismo nativo per chiedere al sistema operativo quali descriptor fossero pronti. Il risultato è stata la dipendenza da stream_select() o da estensioni esterne, e ogni libreria asincrona finiva per scrivere i propri back-end per epoll su Linux, kqueue su BSD, IOCP su Windows, ecc.

La nuova Polling API colma questa lacuna. Espone un sottile wrapper attorno alle funzionalità di polling del sistema operativo (epoll su Linux, kqueue su BSD/macOS, ecc.). L'API non sostituisce le Fibers; fornisce semplicemente all'event loop la velocità e la scalabilità di cui ha bisogno.

Perché il cambiamento è importante per ReactPHP, Amp e simili

ReactPHP e Amp v3 hanno costruito i propri livelli di astrazione sopra i meccanismi di polling del sistema operativo. Questi livelli contengono molteplici percorsi di codice, ciascuno ottimizzato per una piattaforma specifica, e devono essere mantenuti sincronizzati con i cambiamenti del kernel. Con una Polling API nativa, le librerie possono eliminare gran parte di quella complessa infrastruttura e fare affidamento su un'unica chiamata fornita dal core.

  • Manutenzione – Meno rami specifici per piattaforma significano meno bug e una superficie di attacco ridotta per le revisioni di sicurezza.
  • Performance – La chiamata nativa comunica direttamente con epoll/kqueue.
  • Portabilità – Il codice che gira su PHP "vanilla" ottiene ora le stesse prestazioni di base su tutti i sistemi operativi supportati, senza la necessità di estensioni opzionali.

Swoole, al contrario, rimane un sostituto completo del runtime che include il proprio event loop e sistema di coroutine. La Polling API non influisce sui compromessi di Swoole; gli sviluppatori che necessitano di latenza ultra-bassa o di una gestione personalizzata della memoria continueranno a considerarlo un'opzione separata.

Un rapido benchmark racconta la storia

Uno scheduler minimale ha recuperato diversi URL tramite socket grezzi, utilizzando un singolo Io\Poll\Context per gestire più fiber. Sono emerse due osservazioni:

  1. Velocità – L'aggiunta di ulteriori richieste concorrenti non aumenta il tempo totale trascorso; il batch di richieste termina con la stessa velocità della singola richiesta più lenta. In altre parole, l'overhead di concorrenza è effettivamente zero.
  2. Costo della CPU – Con stream_select(), l'utilizzo della CPU aumenta sensibilmente al crescere del numero di stream, poiché la funzione deve iterare su ogni descriptor ad ogni chiamata. Il costo della Polling API rimane costante da tre a trenta stream, grazie alla capacità del kernel di monitorare molti descriptor in un'unica chiamata di sistema.

Cosa dovrebbero fare gli sviluppatori ora

  • Adottare Amp v3 se preferisci uno stile nativo per le fiber che si allinea direttamente con la nuova API. La sua interfaccia pubblica è già mappata sul poller sottostante, quindi ne trai beneficio senza dover riscrivere il codice.
  • Continuare con ReactPHP se preferisci un controllo esplicito sul ciclo di vita del loop.
  • Evitare scheduler personalizzati per carichi di lavoro in produzione. Non scrivere il tuo scheduler per la produzione.