Paralelní zpracování

Školení, která pořádám

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

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";
}
?>

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

Jakub Vrána, Seznámení s oblastí, 23.5.2005, diskuse: 18 (nové: 0)

Diskuse

korny:

nevim proc ale mam vzdy jenom jeden vystup...
a to za seznamu :-(

<?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) {
    $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
}

?>

korny:

Apache/2.0.54 (Mandriva Linux/PREFORK-13mdk)
PHP Version 5.0.4
pcntl support    enabled

prijde mi ze to zpracuje pouze tu nejrychlejsi(seznam) a ostatni pak nic..

muj vystup: Seznam.cz: nenalezeno

Doser:

Chápu, že článek už je starší, nicméně nic novějšího o cURL nikde není... rád bych opravil z vlastní zkušenosti
<?php
   
if ($running && strtoupper(substr(PHP_OS, 0, 3)) == "WIN") {
        sleep(1); // pod Windows curl_multi_select() nečeká
    }
?>
bych opravil na
<?php
   
if ($running && strtoupper(substr(PHP_OS, 0, 3)) != "WIN") {
        sleep(1); // pod Linuxem curl_multi_select() nečeká
    }
?>
přesně tak se mi to chová na WIN XP SP2, multi_select čeká, ale pod Linuxem nikoli. Na Linux serveru libcurl/7.12.1 OpenSSL/0.9.7a zlib/1.2.1.2 libidn/0.5.6, na Win lokále      libcurl/7.16.0 OpenSSL/0.9.8d zlib/1.2.3.
Navíc mám neřešitelný problém s cURL, kdy na windows funguje vše bez problémů, ale na Linuxu mi při multi selectu se některé spojení jakoby zaseknou a ačkoli stránky už jsou kompletně vykonány tak multi select stále čeká. Problém je reprodukovatelný cca. v 1 ze 4 shodných volání. Kdyby někdo tušil, kde je problém, tak budu vděčný za radu...
Stěžejní kód zde:
<?php
       
do{
                $returncode = curl_multi_exec($service_handle,$running);
        } while ($returncode == CURLM_CALL_MULTI_PERFORM);
       
       
//get OS name
        $notwinos = (strtoupper(substr(PHP_OS,0,3)) != 'WIN');
       
       
//still running and ok performed
        while ($running && $returncode == CURLM_OK){
            //get number of descriptors           
            if(curl_multi_select($service_handle,1) != -1){
                if($notwinos){
                    //sleep
                    sleep(1);
                }
                                               
               
do{
                    $returncode = curl_multi_exec($service_handle,$running);                   
               
} while ($returncode == CURLM_CALL_MULTI_PERFORM);
            }
        }
?>

ikona Jakub Vrana OpenID:

Asi to tedy nebude tak docela operačním systémem, protože mě na Windows XP SP2 se standardní distribucí PHP 5.2.5 curl_multi_select() skutečně nečeká.

Xavy:

nevyřešil jste nějak již ten problém, kdy vám pod linuxem nechtelo jít curl paralelní zpracování stejně jako pod windows ? Mám ten samý problém, u mě na apachi na windowsu mi to chodí nádherně, rychle, paralelně. Ovšem na linuxu se to chová podivně. Někdy to sice jde aspon blbě, ale většinou vůbec.

ikona zakjan:

Občas se stane, že CURL nepojme všechny requesty najednou (výstup některých vrací prázdný řetězec). Možná to závisí na vlastnostech připojení k internetu, nevím.
Řešení, které mi funguje, je zde: http://www.onlineaspect.com/2009/01/26/how-…-without-blocking/

Fox:

curl_multi_select()  - celá deklarace je
<php int curl_multi_select  ( resource $mh  [, float $timeout= 1.0  ] )?>
a timeout je "Time, in seconds, to wait for a response.". Takže vykonání původního kódu je o 1 vteřinu delší, než by bylo nutné. Místo curl_multi_select() na začátku je asi lepší použít její volání až za curl_multi_exec() http://php.ftp.cvut.cz/manual/cs/function.…-exec.php#88453 nebo http://cz2.php.net/manual/en/function.curl-multi-exec.php#80977.
A dále - v jednom příspěvku uvedený problém "zaseknutí" zřejmě souvisí s problémem uvedeným například na http://www.secnow.de/websearch/websearch.txt? .

ikona Jakub Vrána OpenID:

Nemyslím si, že by kód běžel o vteřinu déle – pokud funkce curl_multi_select() zjistí nějakou událost, tak nečeká. Timeout je jen pro případ, že se ještě nic nestalo.

Fox:

Ano, curl_multi_select() čeká jen do doby, než se objeví událost - v tom jsem se mýlil. Ale potom je jí zřejmě potřeba použít až za curl_multi_exec(), protože ve vašem případě při prvním průchodu cyklu čeká na událost, aniž by tato událost mohla ještě nastat. Tedy - řešení například z PHP manuálu http://php.ftp.cvut.cz/manual/cs/function.…-exec.php#80977. Ověřoval jsem to ve vašem skriptu a zřejmě to tak bude. Nakonec ale - podobně jednoduchá řešení jako vaše se mi po experimentech http://www.newsroom.cz/php-javascript/55-…-knihovnou-curl ukázala jako nejrychlejší.

Lukáš Břečka:

Ahojte,
mám jeden krátký dotaz.
Má smysl snažit se o paralelní zpracování pomocí PCNTL na serveru s 1 CPU s jedním jádrem?
dle mého názoru to smysl nemá.

díky moc

ikona Jakub Vrána OpenID:

Podle toho u jaké úlohy. Pokud úloha většinu času na něco čeká, tak se to vyplatit může.

Lukáš Břečka:

většinou se čeká na provedení insertu nebo selectu v DB.
Zkusím to tedy pomocí PCTNL. Třeba to pomůže.

Díky

ikona v6ak:

Vyplatit se to může. Ale ono hlavně nezáleží ani tak na počtu jader, protože k dobře zatíženému serveru přistupuje v praxi asi více uživatelů zároveň.

Howkez:

Chtěl bych se zeptat pomocí jaké funkce se pošle můj požadavek a jak zjistím, že můj požadavek byl vyřízený a mohu stáhnout data? díky

HosipLan:

Zdravím,
oceňuji článek, vzhledem k tomu, že chci implementovat MultiRequest do svého wrapperu na cURL, ale ještě jsem se k tomu nedostal. Budu se snažit poskládat z článku a komentářů funkční řešení :)

kdyby někdo chtěl kouknout na současnou verzi: http://github.com/HosipLan/cURL-wrapper :)

ikona Jakub Vrána OpenID:

Novější iterace kódu a funkční řešení je na http://github.com/vrana/php-async/blob/master/CurlAsync.php. Pro inspiraci je tedy lepší použít to.

Jirka:

Ahoj, mám jeden dotaz: Představ si, že skript v příkladu funguje trochu jinak - hodnotu počtu výsledku ukládá někam do databáze pravidelně (třeba se může jednat o nějaké online statistiky).

Ať už skript(nemusí jít o PHP) - proces získávání n (ať je to třeba 10 nebo 500) výsledků z několika serverů poběží konkurečně(react.php,fork,curl_multi,ruby eventmachine) nebo ne, co je lepší?
- Po získání každého výsledku ho zapsat do databáze(celkem se provede n updatů)
- Nebo si hodnoty ukládat do pole a na konci udělat jeden velký update?

Samozřejmě se asi jedná o hledisko zátěž databáze na jedné straně a odolnost vůči výpadkům na druhé straně. (Nebo jsem na něco ještě zapomněl?)
aký je tvůj názor na to?

ikona v6ak OpenID:

V podstatě sis odpověděl. Záleží na tom, co chceme. Pokud máme vytížený DB server a malou pravděpodobnost, že se něco pokazí v průběhu, bude se hodit ukládat vše zároveň. Pokud se DB server fláká a chceme nějak řešit, že v průběhu fetchování se něco pokazí, pak asi využijeme průběžné ukládání.

Vložit komentář

Používejte diakritiku. Vstup se chápe jako čistý text, ale URL budou převedeny na odkazy a PHP kód uzavřený do <?php ?> bude zvýrazněn. Pokud máte dotaz, který nesouvisí s článkem, zkuste raději diskusi o PHP, zde se odpovědi pravděpodobně nedočkáte.

Jméno: URL:

avatar © 2005-2018 Jakub Vrána. Publikované texty můžete přetiskovat pouze se svolením autora. Ukázky kódu smíte používat s uvedením autora a URL tohoto webu bez dalších omezení Creative Commons. Můžeme si tykat. Skripty předpokládají nastavení: magic_quotes_gpc=Off, magic_quotes_runtime=Off, error_reporting=E_ALL & ~E_NOTICE a očekávají předchozí zavolání mysql_set_charset. Skripty by měly být funkční v PHP >= 4.3 a PHP >= 5.0.