Fibers w PHP - jak ułatwić wdrożenie asynchroniczności w projekcie

sages.pl 1 rok temu
Z tego artykułu dowiesz się: * Jakie problemy, może sprawić przepisanie jednej funkcjonalności na asynchroniczną * Czym jest nowy mechanizm w PHP 8.1 - Fibers * Jak Fibers, może Ci pomóc we wprowadzaniu asynchroniczności w twój projekt ## Asynchroniczność w PHP Język, jakim jest PHP, przyzwyczaił nas do programowania jednowątkowego. Najczęściej funkcje wywołujemy po kolei w taki sposób, iż każda kolejna funkcja czeka z rozpoczęciem, dopóki poprzednia nie zwróci rezultatu. Jednakże kod nie musi być wykonywany linijka po linijce, w jednym wątku. W wersji czwartej języka pojawiła się możliwości tworzenia kodu asynchronicznego. Z początku jedyną możliwością było tworzenie forków poprzez funkcję `pcntl_fork`. Szczęśliwie, dziś kilka osób korzysta z tej metody, która polegała na dość skomplikowanym zarządzaniu wieloma procesami. Metoda ta, wbudowana w język PHP dzieliła istniejący proces na dwa oddzielne. Komunikacja między tymi procesami była bardzo utrudniona i łatwo było zająć zbyt dużo zasobów maszyny, na których te procesy były wykonywane. Dla ułatwienia stworzono wiele bibliotek, których dobrym przykładem może być biblioteka [*spatie/async*](https://github.com/spatie/async/). W PHP 5.5 dodano słowa najważniejsze - *yield*. Słowo to pozwala na tworzenie generatora. Pozwalają one na nielinearne wykonywanie kodu. Dzięki temu powstało wiele bibliotek wspomagających programowanie asynchroniczne. Przykładem może być [*Guzzle/promises*](https://github.com/guzzle/promises). Przyjrzyjmy się różnicy pomiędzy podejściem synchronicznym a asynchronicznym w PHP. W tym celu posłużmy się pewnym przykładem: *Załóżmy, iż chcemy ściągnąć treść trzech stron i połączyć je ze sobą w jedną zmienną. Nasz synchroniczny kod mógłby wyglądać w ten sposób:* ``` $sites = [ 'http://sages.com.pl/', 'http://sages.com.pl/szkolenia', 'http://sages.com.pl/blog', ]; function downloadSite($siteUrl) { $client = new GuzzleHttp\Client(); return $client->get($siteUrl)->getBody(); } function downloadAllSites($sites): string { $sitesContent = []; foreach ($sites as $siteUrl) { $sitesContent[] = downloadSite($siteUrl); } return implode('
', $sitesContent); } $return = downloadAllSites($sites); ``` Jak widać ściągamy treść każdej ze stron po kolei. Na koniec łączymy wszystko w jedno. o ile chcielibyśmy żeby ściąganie plików działało asynchronicznie, to wystarczy metodę `get` zamienić na `getAsync`. Metoda ta zwraca nam obiekt `Promise`. Obiekt ten reprezentuje wynik, który zostanie zwrócony po wywołaniu asynchronicznej funkcji. ``` function downloadSite($siteUrl) { $client = new GuzzleHttp\Client(); return $client->getAsync($siteUrl); } function downloadAllSites($sites) { $results = []; foreach ($sites as $siteUrl) { $results[] = downloadSite($siteUrl); } //return ?? //i co tu możemy zwrócić? } ``` Niestety mając zwrócone obiekty typu *promise*, nie możemy ich połączyć do momentu, w którym wszystkie te funkcje się nie wykonają tak, jak nie możemy zwrócić obiekt typu *promise*. Problem ten świetnie opisuje artykuł [*Jakiego koloru jest Twoja funkcja?*](https://journal.stuffwithstuff.com/2015/02/01/what-color-is-your-function/). W bardzo dużym uproszczeniu ww. artykuł opisuje zalety i wady programowania asynchronicznego. Przestrzega przed asynchronicznością, wskazując przede wszystkim na to, iż wywołanie funkcji asynchronicznej zmusza nas do traktowania całego stacka funkcji jako asynchronicznego. Odpowiedzią na ten problem, w wersji PHP 8.1 jest **Fibers** - rozwiązanie, z powodzeniem stosowane już w języku Ruby, które pozwala nam zastosować asynchroniczność bez przepisywania całego kodu. Oczywiście, jak widać na powyższym przykładzie, moglibyśmy odczekać, aż nasze funkcje asynchroniczne się wykonają, wywołując na nich metodę `wait`, ale to rozwiązanie pomijam, bo przez nie tracimy korzyści, wynikające z asynchroniczności. ## Fibers Fibers reprezentują przerywalne funkcje. Mogą być przerwane w dowolnym momencie i pozostać zawieszone do czasu ich wznowienia. Najlepiej przedstawia to przykład z dokumentacji PHP: ``` $fiber = new Fiber(function (): void { $value = Fiber::suspend('fiber'); echo "Value used to resume fiber: ", $value, PHP_EOL; }); $value = $fiber->start(); echo "Value from fiber suspending: ", $value, PHP_EOL; $fiber->resume('test'); ``` Przykład ten wyświetli nam: *Value from fiber suspending: fiber* *Value used to resume fiber: test* Przyjrzyjmy się, co w tym przykładzie dzieje się po kolei. Do konstruktora klasy `Fibers` przekazujemy callback, która będzie wywołana w momencie uruchomienia metody `start()`. najważniejsze w tej funkcji jest wywołanie `Fiber::suspend()`. Przerywa to działanie funkcji przekazanej w konstruktorze. Funkcja jest wznawiana dopiero w momencie wywołania metody `resume `na obiekcie `“fiber”`. Innymi ciekawymi metodami klasy `Fibers` są: * `isTerminated`, informująca nas, czy callback przekazany do konstruktora się już wykonała, * `getReturn`, zwracająca to samo, co callback po wykonaniu. Gdy pierwotnie zobaczyłem ten przykład, nie do końca rozumiałem, jakie może być jego zastosowanie. Odpowiedź odnalazłem, dopiero czytając RFC. Fibers są stworzone po to, by można było wywoływać funkcje asynchroniczne, bez przepisywania całego stosu wywołań funkcji. A zatem jest to odpowiedź na problem opisany na początku tego wpisu oraz w artykule [*Jakiego koloru jest Twoja funkcja?*](https://journal.stuffwithstuff.com/2015/02/01/what-color-is-your-function/). Spróbujmy napisać asynchroniczny kod, który równie dobrze mógłby się znaleźć wewnątrz funkcji synchronicznej: ``` use Spatie\Async\Pool; $fiber = new Fiber(function (): string { $processes = [ 'operacja 1', 'operacja 2', 'operacja 3', ]; $pool = Pool::create(); $result = new stdClass(); $result->result = ''; foreach ($processes as $processName) { $pool->add(function() use($processName){ $operationTime = rand(1, 15); sleep($operationTime); return $processName . ' zajeła ' . $operationTime . ' sekund' . PHP_EOL; }) ->then(function($output) use ($result) { $result->result .= $output; }); } while (count($pool->getFinished()) !== count($processes)) { Fiber::suspend(); $pool->notify(); } return $result->result; }); $value = $fiber->start(); while ($fiber->isTerminated() === false) { sleep(1); echo 'W tym miejscu w kodzie, możesz dokonać dowolną operację'. PHP_EOL; $fiber->resume(); } echo $fiber->getReturn(); ``` Nasz kod co jakiś czas sprawdza, czy asynchroniczne wywołania już się wykonały. A w międzyczasie pozwala nam na wykonywanie własnego kodu. Ten przykład, na standardowym wyjściu, wyświetli nam mniej więcej taki rezultat: ``` W tym miejscu w kodzie, możesz dokonać dowolną operację W tym miejscu w kodzie, możesz dokonać dowolną operację W tym miejscu w kodzie, możesz dokonać dowolną operację W tym miejscu w kodzie, możesz dokonać dowolną operację W tym miejscu w kodzie, możesz dokonać dowolną operację W tym miejscu w kodzie, możesz dokonać dowolną operację W tym miejscu w kodzie, możesz dokonać dowolną operację W tym miejscu w kodzie, możesz dokonać dowolną operację operacja 1 zajeła 4 sekund operacja 3 zajeła 5 sekund operacja 2 zajeła 7 sekund ``` ## Podsumowanie Fibers są mało znanym i mało opisywanym mechanizmem PHP. o ile wejdziemy głębiej w ten temat, okazuje się, iż dają nam rozwiązanie na dość typowy problem przy programowaniu asynchronicznym. Dzięki nim możemy dodać w jednym punkcie naszej aplikacji asynchroniczność, bez konieczności przepisywania całej aplikacji.
Idź do oryginalnego materiału