Hlavní navigace

PHP okénko: Paralelní zpracování

23. 5. 2005
Doba čtení: 4 minuty

Sdílet

Dnešní PHP okénko představuje na jednoduchém příkladu možnosti PHP při paralelním načítání dat a ukazuje jeden nový obrat v PHP 5.

Ř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_ad­d_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:

UX DAy - tip 2

<?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.

Byl pro vás článek přínosný?

Autor článku

Autor se živí programováním v PHP, podílí se na jeho oficiální dokumentaci, vyučuje ho na MFF UK a vede odborná školení. Poznámky si zapisuje na weblog PHP triky.