Odată cu versiunea 8.1, PHP a introdus fibers1. Dar fibers, nu trebuie confundat cu fire de execuție, respectiv threads. Așadar pentru a elimina confuzia, și a ne face o imagine despre conceptul de asynchronous code, o să vă arăt în aceste rânduri câteva exemple practice:
Ce sunt fibers?
În PHP, fibers sunt o strategie de a grăbi execuția unor task-uri, iar fibrele se pot executa și în paralel - de unde și confuzia cu fire de execuție. Însă, spre deosebire de threads, fibers nu execută cod PHP în paralel - PHP este single-threaded. Dar ne permit să ”împachetăm” un task extern, care poate fi executat în paralel.
Fibers sunt eficace doar dacă se îndeplinesc următoarele condiții:
Interacționezi cu resurse externe aplicației, al căror status poate fi procesat în paralel cu rularea aplicației. De exemplu un sub-proces sau sub-shell sau o cerere în rețea.
Poți cere sau interacționa cu resursa, fără să blochezi execuția codului. De exemplu o cerere către baza de date, de a cărui rezultat depinde o condiție ulterioară, nu poate fi executată în paralel - codul așteaptă rezultatul să continue.
Exemplu practic: generarea de video din imagini cu ffmpeg
Pentru o demonstrație, ne propunem să generăm câte un clip video (un reel) pentru imaginile de cu extensia jpg, salvate în trei directoare, folosind utilitarul ffmpeg
2.
Imaginile mele sunt plasate, în directoarele clip1, clip2, și clip3 sub directorul assets al proiectlui:
Pentru a converti fiecare dintre aceste colecții de imagini într-un video, unul după altul sincron, aș putea folosi următorul cod:
Pe laptopul meu se execută destul de rapid, respectiv în 2.9 secunde:
4 folders processed in 2.9 seconds
Un exec care nu blochează
exec
-ul folosit mai sus, este o operațiune care blochează - se convertește fiecare foder unul după altul. În ordinea definită. Pentru generarea clipului 4 (clip4.mp4), așteptăm prelucrarea tuturor celor 3 foldere precedente. Pentru a folosi fibers, mai întâi este necesar să convertim aceste exec-uri la o variantă care nu blochează și să o exportăm într-o funcție.
Avem nevoie de proc_open
3: acesta nu blochează execuția scriptului până la finalizarea comenzii ffmpeg, ci lansează un shell pe lângă script-ul de php la care ne putem conecta, la nevoie pentru a verifica statusul comenzii:
Veți observa că din perspectiva iterației, acest cod este tot unul blocant, deoarece am introdus un while
și un usleep
. Dar am introdus intenționat aici bucata de cod blocantă, care o vom elimina prin introducerea fibers.
Să ”depănăm” cu fibre
Acum că avem o funcție care nu blochează, puteam crea câte un fiber pentru fiecare director din care dorim să generăm clip:
Dacă am pus fiecare generare într-o fibră se vor executa în paralel și mult mai rapid, nu? Ei nu încă, pentru că nu am eliminat partea blocantă din funcția noastră createFolderClip
și fibrele vor rula codul asincron doar cu Fiber::suspend()
.
PHP Asincron
În locul lui usleep
introducem suspend
. Și pentru a putea obține statusul fără să așteptăm în foreach-ul care lansează fibrele, le adăugăm într-o listă și le rezumăm într-o buclă:
4 folders processed in 0.7 seconds
Acum pot converti cele 4 foldere în mai puțin de o secundă!
Paralelism, dar cu măsură
Codul de mai sus a convertit doar 4 directoare. Toate au rulat în paralel, fiecare pe câte unul din ce 12 procesoare de care dispune mașina mea. Însă dacă am generaliza codul să ruleze pe un număr variabil de directoare, va fi necesar să limităm numărul de fibre care sunt rulate în același timp, pentru a evita context switching4. În exemplul de mai sus am putea implementa asta accesând $fiber→resume()
doar pentru 12 fibre odată, și pe măsură ce acetea returnează, să rezumăm următoarele.
Puteți găsi codul din exemple pe github: https://github.com/moustacios/php8-tutorials/tree/main/php8-fibers