Asinhrono programiranje u PHP-u
U tradicionalnom PHP programiranju, vaš kod se izvršava sekvencijalno, redom kojim je i napisan. To znači da svaka instrukcija unutar koda mora da se završi pre nego što sledeća počne, odnosno da se zadaci obrađuju jedan po jedan, bez preklapanja.
Ovakav model je jednostavan i pouzdan za manje skripte i web aplikacije ograničenog obima. Međutim, kada aplikacija mora da obradi više dugotrajnih zadataka istovremeno, kao na primer kod mrežnih zahteva, upita ka bazi podataka ili učitavanja velikih fajlova, ovakav pristup postaje neefikasan i često usporava ceo sistem.
Zamislite neku aplikaciju koja mora da pošalje desetine HTTP zahteva, pročita više fajlova i za to vreme ne koči u radu. Ako bi svaka od tih operacija čekala da se prethodna završi, korisnik bi imao utisak da aplikacija zamrzava i radi previše sporo. Upravo zato je asinhrono programiranje postalo veoma popularno i u PHP ekosistemu.
Iako PHP nema ugrađenu nativnu asinhronu podršku kao JavaScript ili Python, srećom su nam na raspolaganju biblioteke kao što su ReactPHP i Amp. Ove dve biblioteke omogućavaju konkurentno i delimično paralelno izvršavanje koda, bez blokiranja glavnog toka aplikacije.
Šta znači neblokirajući kod?
Ključna ideja asinhronog programiranja je neblokirajući I/O. Umesto da aplikacija privremeno zastane dok se čeka rezultat neke operacije (poput HTTP zahteva), ona nastavlja dalje, a kada na kraju rezultat stigne, ona ga odmah obrađuje.
Recimo da šaljemo zahtev eksternom API-ju. U tradicionalnom PHP-u, koristili bismo funkcije poput file_get_contents() ili curl_exec(), koje bi zadržale izvršavanje dok se ne dobije odgovor. U asinhronom pristupu, zahtev se šalje, ali aplikacija ne čeka, već može da prikaže poruku korisniku, obradi drugu logiku ili pripremi sledeće korake.
Hajde da vidimo kako to izgleda kroz jednostavan primer sa ReactPHP:
require 'vendor/autoload.php';
$loop = React\EventLoop\Factory::create();
$client = new React\Http\Browser($loop);
$client->get('https://api.example.com/data')
->then(function (Psr\Http\Message\ResponseInterface $response) {
echo "Odgovor: " . $response->getBody();
});
echo "Ova linija se izvršava odmah, dok čekamo odgovor API-ja.\n";
$loop->run();
Ovde šaljemo HTTP zahtev, ali izvršavanje se ne zaustavlja. Linija sa echo se prikazuje odmah, dok se mrežni zahtev još uvek izvršava. Kada odgovor stigne, automatski se aktivira funkcija definisana u then() bloku.
Ovakvo ponašanje je osnova konkurentnog programiranja u PHP-u i omogućava da se više zadataka odvija naizmenično, čak i u aplikacijama koje rade na samo jednom procesoru.
Uloga događajne petlje u asinhronom PHP kodu
Da bi asinhroni kod mogao da funkcioniše, potreban je mehanizam koji će upravljati svim zadacima koji čekaju na završetak. Tu dolazi do izražaja događajna petlja (event loop). Ona konstantno nadgleda sve zadatke i, kada se neki završi pokreće funkciju koja je prethodno vezana za taj događaj.
U ReactPHP-u i Amp-u, događajna petlja je centralni deo svakog asinhronog programa. Ona pokreće sve: od tajmera do mrežnih zahteva.
Na primer, možemo da kreiramo jednostavan tajmer koji prikazuje poruku nakon dve sekunde:
$loop = React\EventLoop\Factory::create();
$loop->addTimer(2, function () {
echo "Ova poruka se pojavljuje nakon 2 sekunde.\n";
});
echo "Ova poruka se pojavljuje odmah.\n";
$loop->run();
Ovo ponašanje jasno pokazuje kako događajna petlja funkcioniše: program ne čeka dve sekunde da bi prešao na sledeću liniju, već nastavlja dalje, dok se zakazana funkcija pokreće nezavisno, kada istekne vreme.
Kako PHP koristi konkurentnost, a ne paralelizam
Iako na prvi pogled može delovati kao da se više zadataka izvršava u isto vreme, zapravo se u većini slučajeva koristi konkurentnost, a ne pravi paralelizam. Konkurentnost znači da se zadaci izvršavaju uporedo, ali naizmenično. Kada jedan zadatak čeka (npr. mrežni odgovor), procesor koristi to vreme da nastavi sa drugim zadatkom.
To se najbolje vidi kod masovnih HTTP operacija. Na primer, umesto da slanjem 100 zahteva čekamo da se svaki završi pojedinačno, možemo da ih sve pokrenemo odmah i dozvolimo događajnoj petlji da ih obradi kako stignu:
$loop = React\EventLoop\Factory::create();
$client = new React\Http\Browser($loop);
$urls = ['https://api1.com', 'https://api2.com', 'https://api3.com'];
$promises = [];
foreach ($urls as $url) {
$promises[] = $client->get($url)->then(function ($response) use ($url) {
echo "Završeno: $url\n";
});
}
React\Promise\all($promises)->then(function () {
echo "Svi zahtevi su uspešno završeni.\n";
});
$loop->run();
U ovom primeru, svi zahtevi su pokrenuti gotovo istovremeno. PHP koristi neblokirajući model da ih smesti u događajnu petlju, koja ih sukcesivno obrađuje kad stignu odgovori.
Nasuprot konkurentnosti, paralelizam podrazumeva da se zadaci zaista izvršavaju istovremeno, na više procesora ili jezgara. PHP to ne podržava po default-u, ali je moguće kroz funkcije kao što su pcntl_fork() ili proc_open(), koje omogućavaju rad sa više procesa.
Na primer, možemo da obradimo slike u paralelnim procesima:
$pid = pcntl_fork();
if ($pid == -1) {
die("Greška pri forkovanju");
} elseif ($pid) {
pcntl_wait($status); // parent čeka
} else {
resizeImage('slika.jpg'); // child obrađuje sliku
exit(0);
}
Ovo je korisno za CPU-intenzivne zadatke, dok je za većinu I/O operacija konkurentnost sasvim dovoljna i značajno jednostavnija za implementaciju.
Callback funkcije
Kada govorimo o asinhronom programiranju u PHP-u, jedan od najčešće korišćenih mehanizama za reagovanje na završetak neke operacije jeste callback. U pitanju je funkcija koju prosleđujemo kao argument, a koja se poziva kada dođe do određenog događaja. Na ovaj način možemo jasno da definišemo šta aplikacija treba da uradi kada, recimo, stigne rezultat HTTP zahteva, fajl bude pročitan ili istekne tajmer.
U ReactPHP-u i drugim asinhronim bibliotekama, callback funkcije su osnov svega, jer omogućavaju dinamičko ponašanje aplikacije. Umesto da aplikacija stoji i čeka, mi joj unapred „kažemo“: kad završiš — pozovi ovo.
Evo jednog jednostavnog primera callback funkcije:
function pozdrav($ime, callable $callback) {
echo "Zdravo, $ime!\n";
$callback();
}
pozdrav("Nenad", function () {
echo "Ovo je pozvano kao callback.\n";
});
Ovaj obrazac može da se proširi i na asinhrone zadatke. Na primer, možemo da simuliramo čitanje fajla u sledećem koraku događajne petlje:
function readFileAsync($filename, $callback) {
$loop = React\EventLoop\Factory::create();
$loop->futureTick(function () use ($filename, $callback) {
$data = file_get_contents($filename); // u realnom primeru bi bila asinhrona operacija
$callback($data);
});
$loop->run();
}
readFileAsync("test.txt", function ($data) {
echo "Sadržaj fajla: $data\n";
});
Iako su callback funkcije jednostavan i moćan alat, imaju jednu ozbiljnu manu: kada se više asinhronih zadataka zavisi jedan od drugog, lako upadamo u tzv. callback hell. U pitanju je situacija gde se više funkcija ugnjezdi jedna u drugu, pa kod postaje teško čitljiv i još teže održiv.
Upravo da bi se prevazišao taj problem, uvedeni su modeli sa obećanjima (promises) i async/await stilom pisanja koda.
Obećanja (Promises)
Obećanje, u kontekstu asinhronog programiranja, predstavlja objekat koji simbolizuje vrednost koja još uvek nije dostupna, ali će to biti u nekom trenutku u budućnosti.
Kako smo već pomenuli u prethodnom poglavlju, kada radimo sa asinhronim operacijama, često koristimo tzv. callback funkcije — funkcije koje se prosleđuju kao argumenti i pozivaju nakon što se neka operacija završi. Ako više takvih operacija zavisi jedna od druge, često završimo sa ugnežđenim funkcijama unutar funkcija, što otežava čitanje i održavanje koda.
Zato se koriste obećanja (promises). U pitanju je mehanizam koji omogućava da asinhroni tok organizujemo kao niz uzastopnih koraka, pomoću metoda kao što su then(), catch() i finally(). Umesto da svaka sledeća funkcija bude ugurana unutar prethodne, koraci se ređaju jedan iza drugog u jasnom linearnom obliku. To čini kod čistijim, preglednijim i lakšim za održavanje..
U ReactPHP biblioteci, gotovo svaka metoda koja inicira asinhronu operaciju vraća promise. To nam omogućava da elegantno povežemo šta treba da se dogodi kada rezultat stigne, bez zadržavanja toka izvršavanja.
Na primer, slanje više HTTP zahteva i obrada njihovih rezultata izgleda ovako:
$client = new React\Http\Browser(React\EventLoop\Factory::create());
$promises = [
$client->get('https://api1.com'),
$client->get('https://api2.com')
];
React\Promise\all($promises)->then(function ($responses) {
foreach ($responses as $response) {
echo "Odgovor: " . $response->getBody() . "\n";
}
});
Prednost ovakvog pristupa je što sve izgleda znatno čistije i linearnije u odnosu na višeslojne callback-e. Takođe, zahvaljujući metodama catch() i finally(), možemo jasno definisati kako da reagujemo na greške i kada se sve završi, bez obzira na ishod.
Ipak, kako broj operacija i zavisnosti među njima raste, čak i kod sa promises počinje da gubi preglednost. U tom trenutku, async/await pristup postaje sledeći logičan korak.
async/await stil sa Amp bibliotekom
Dok JavaScript i Python imaju ugrađenu podršku za async i await sintaksu, PHP još uvek nema tu mogućnost. Ipak, biblioteka Amp koristi generatore i korutine da simulira takvo ponašanje u PHP-u. Time vam omogućava pisanje asinhronog koda koji izgleda gotovo identično kao običan, sinhroni kod — linija po linija, bez then lanaca i bez callback pakla.
Evo jednog kompletnog primer asinhronog HTTP zahteva sa Amp-om:
use Amp\Loop;
use Amp\Http\Client\HttpClientBuilder;
use Amp\Http\Client\Request;
Loop::run(function () {
$client = HttpClientBuilder::buildDefault();
$request = new Request('https://api.example.com/data');
try {
$response = yield $client->request($request);
$body = yield $response->getBody()->buffer();
echo "Odgovor sa servera: $body\n";
} catch (\Throwable $e) {
echo "Greška: {$e->getMessage()}\n";
}
});
U ovom primeru sve izgleda kao da se radi sinhrono: šaljemo zahtev, čekamo odgovor, čitamo telo. Ali iza kulisa, Amp koristi yield da pauzira izvršavanje dok se operacija ne završi, a zatim nastavlja kod kada su rezultati spremni.
Prednosti ovog pristupa su ogromne u složenim aplikacijama: veća čitljivost, jednostavnije rukovanje greškama i manja šansa da napravimo logičku grešku u asinhronom toku.
ReactPHP ili Amp
I ReactPHP i Amp rešavaju isti problem: kako omogućiti asinhrono programiranje u jeziku koji to ne podržava nativno. Ali razlika je u stilu i načinu izražavanja.
ReactPHP se oslanja na promises, podseća na JavaScript i daje veliku kontrolu programeru. Svaki korak mora biti eksplicitno definisan, što je moćno, ali može postati nepregledno u većim aplikacijama.
Amp, sa druge strane, koristi sintaksu baziranu na yield i nudi stil koji je bliži običnom PHP pisanju. Programeri ne moraju da razmišljaju o lančanju then poziva, već pišu asinhroni kod kao da je sinhron, što je u mnogim slučajevima čitljivije i lakše za održavanje.
Na primer, ista logika kao u prethodnom Amp primeru u ReactPHP-u bi izgledala ovako:
$loop = React\EventLoop\Factory::create();
$client = new React\Http\Browser($loop);
$client->get('https://api.example.com/data')->then(
function (Psr\Http\Message\ResponseInterface $response) {
echo $response->getBody();
},
function (Exception $error) {
echo "Greška: " . $error->getMessage();
}
);
$loop->run();
Oba primera rade istu stvar, ali stil i ergonomija koda su očigledno različiti.
Zaključak
Asinhrono programiranje u PHP-u više nije eksperiment ili teoretska mogućnost. Ono je praktičan alat za gradnju modernih aplikacija koje zahtevaju brzu obradu podataka, više istovremenih zahteva i neprekidno reagovanje na događaje.
Ako želite potpunu kontrolu, ReactPHP je odličan izbor. Ako Vam je prioritet čitljivost i prirodniji tok izvršavanja, Amp će Vam olakšati razvoj i održavanje.
Bez obzira na to koji alat odaberete, koristi su jasne: efikasniji rad sa resursima, brži odziv sistema, veća skalabilnost i bolji korisnički doživljaj. Što je najbolje, sve to dobijate unutar već dobro poznatog PHP okruženja.
