Ř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.
Využití knihovny CURL
Ř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";
}
?>
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.
Podobně laděné texty můžete najít i na autorově weblogu PHP triky.