Článek vyšel v rámci PHP okénka na serveru Root.cz.
Řekněme, že bychom chtěli vytvořit kód zjišťující, kolik jednotlivé vyhledávače vrací výsledků při hledání různých slov. Když pominu omáčku okolo, mohl by kód vypadat nějak takhle:
<?php $vyhledavace = array( // nazev => array(url, pattern), ... 'Seznam.cz' => array('http://search.seznam.cz/search.cgi?w=', '<b>([ 0-9]+)</b> nalezen'), 'Centrum.cz' => array('http://search.centrum.cz/index.php?q=', 'Nalezeno <strong>([ 0-9]+)</strong>'), 'Atlas.cz' => array('http://search.atlas.cz/default.aspx?q=', '<b>([ 0-9]+)</b></span> nalezen'), 'Jyxo.cz' => array('http://jyxo.cz/s?s=', 'Jyxo nalezlo <b>([0-9]+)</b>'), 'Google.com' => array('http://www.google.com/search?q=', 'of about <b>([,0-9]+)</b>'), ); foreach ($vyhledavace as $nazev => $vyhledavac) { $file = file_get_contents($vyhledavac[0] . urlencode($_GET["slovo"])); echo "$nazev: " . (preg_match("~$vyhledavac[1]~", $file, $matches) ? $matches[1] : "nenalezeno") . "\n"; } ?>
K dokonalosti kódu ještě leccos chybí (různé vyhledávače např. používají různé kódování a je jim potřeba posílat některé hlavičky), ale o tom teď psát nechci. Při spuštění si nejde nevšimnout toho, že skript většinu času jenom čeká na stažení dat a je proto zbytečně pomalý. Rozdělením toku do vláken PHP nedisponuje, ale díky rozšíření PCNTL je možné tok kódu rozdělit do nezávislých procesů. Princip je stejný jako v jazyce C – funkce pcntl_fork vytvoří nový proces, rodičovi vrátí číslo procesu potomka a potomkovi nulu. Kód tedy upravíme tak, že rodič vytvoří pro každý vyhledávač samostatný proces a dotaz do vyhledávače bude klást tento potomek. Tím dosáhneme paralelního zpracování.
<?php foreach ($vyhledavace as $nazev => $vyhledavac) { $pid = pcntl_fork(); if (!$pid || $pid == -1) { // potomek nebo nelze forknout $file = file_get_contents($vyhledavac[0] . urlencode($_GET["slovo"])); echo "$nazev: " . (preg_match("~$vyhledavac[1]~", $file, $matches) ? $matches[1] : "nenalezeno") . "\n"; if (!$pid) { // pro potomka práce skončila exit; } } } while (pcntl_wait($status) > 0) { // počkáme na dokončení všech dětí, ať se z nich nestanou zombie } ?>
Výsledky z jednotlivých vyhledávačů se budou vypisovat v tom pořadí, v jakém budou získány. Pokud bychom místo toho chtěli data z dětí předávat rodičovi, hodila by se funkce msg_send, ale o tom zase až někdy příště.
Rozšíření PCNTL je za normální okolností k dispozici pouze pro systémy typu Unix, na Windows můžete použít CygPHP – speciální kompilaci, která obsahuje mimo jiné i toto rozšíření. Rozšíření je navíc k dispozici pouze v prostředích CGI a CLI, takže se dá použít hlavně při spouštění skriptů z příkazové řádky – při použití na webu běží PHP obvykle jako modul webového serveru.
Řešení s přidáváním procesů je univerzálně použitelné, ale poměrně nehospodárné. Pokud nám jde pouze o paralelní načítání dat ze sítě, existují i jiné prostředky. Alternativní způsob řešení podobného problému s využitím asynchronního připojení popisuje na svém blogu Wez Furlong. Jeho řešení je hodně nízkoúrovňové, já proto ukážu, jak se tento problém dá řešit s využitím rozšíření CURL a funkcí curl_multi_*, které byly přidány v PHP 5:
<?php $curl_multi = curl_multi_init(); foreach ($vyhledavace as &$val) { $val[2] = curl_init($val[0] . urlencode($_GET["slovo"])); curl_setopt($val[2], CURLOPT_RETURNTRANSFER, true); curl_multi_add_handle($curl_multi, $val[2]); } unset($val); ?>
V první části se s využitím funkce curl_multi_add_handle inicializuje multi stack. Cyklus foreach využívá novou vlastnost PHP 5 – proměnná $val je předávaná referencí, takže změny v ní se projeví přímo v poli. Po skončení cyklu je vhodné tuto proměnnou zrušit, protože jinak se do ní přiřazené hodnoty promítnou i do posledního prvku pole. Identifikátor připojení ještě budeme potřebovat, proto se ukládá jako třetí prvek jednotlivých polí v proměnné $vyhledavace.
<?php do { curl_multi_select($curl_multi); while (CURLM_CALL_MULTI_PERFORM == curl_multi_exec($curl_multi, $running)) { // provést znovu } if ($running && strtoupper(substr(PHP_OS, 0, 3)) == "WIN") { sleep(1); // pod Windows curl_multi_select() nečeká } } while ($running); ?>
V druhé části se funkcí curl_multi_select čeká na dostupnost dat a funkcí curl_multi_exec se tato data přijímají. Na Windows funkce curl_multi_select bohužel skončí okamžitě, takže čekání zajistíme alespoň funkcí sleep.
Po stažení všech dat se již jen vypíšou výsledky:
<?php foreach ($vyhledavace as $nazev => $val) { $file = curl_multi_getcontent($val[2]); echo "$nazev: " . (preg_match("~$val[1]~", $file, $matches) ? $matches[1] : "nenalezeno") . "\n"; } ?>
O něco pohodlnější způsob nabízí třída HttpRequestPool.
Problém paralelního zpracování není v PHP potřeba řešit příliš často – některé úlohy stačí rozdělit na více menších skriptů a jejich paralelní zpracování zajistí přímo webový server. Když na to ale přijde, jsou k dispozici přinejmenším tři možné způsoby. Ideální samozřejmě je, když úloha při čekání na data provádí nějakou výpočetně náročnější operaci – např. kontroluje pravopis nebo hledá syntaktické chyby v dosud stažených souborech.
Přijďte si o tomto tématu popovídat na školení Výkonnost webových aplikací.