zamilované diskusné vlákno php. Viacvláknové programovanie PHP s prekladom Pthreads. Vybavovanie jednorazových úloh

  • 03.11.2019
  • programovanie,
  • Paralelné programovanie
  • Nedávno som vyskúšal pthreads a bol som milo prekvapený – ide o rozšírenie, ktoré do PHP pridáva možnosť práce s niekoľkými skutočnými vláknami. Žiadna emulácia, žiadna mágia, žiadne falzifikáty – všetko je skutočné.



    Pozerám sa na tento problém. Existuje súbor úloh, ktoré je potrebné rýchlo dokončiť. PHP má iné nástroje na riešenie tohto problému, tie tu nie sú spomenuté, článok je o pthreadoch.



    Čo je pthreads

    To je všetko! No skoro všetko. V skutočnosti je tu niečo, čo môže zvedavého čitateľa rozčúliť. Nič z toho nefunguje na štandardnom PHP skompilovanom s predvolenými možnosťami. Aby ste si užili multithreading, musíte mať vo svojom PHP povolené ZTS (Zend Thread Safety).

    Nastavenie PHP

    Ďalej PHP so ZTS. Ignorujte taký veľký rozdiel v čase vykonávania oproti PHP bez ZTS (37,65 vs 265,05 sekúnd), nesnažil som sa zovšeobecňovať nastavenie PHP. V pripade bez ZTS mam povolene XDebug napr.


    Ako vidíte, pri použití 2 vlákien je rýchlosť vykonávania programu približne 1,5-krát vyššia ako v prípade lineárneho kódu. Pri použití 4 vlákien - 3 krát.


    Môžete si všimnúť, že aj keď je procesor 8-jadrový, pri použití viac ako 4 vlákien sa doba vykonávania programu takmer nezmenila. Zdá sa, že je to spôsobené tým, že môj procesor má 4 fyzické jadrá.Pre prehľadnosť som dosku znázornil vo forme schémy.


    Zhrnutie

    PHP umožňuje celkom elegantný multithreading pomocou rozšírenia pthreads. To poskytuje citeľné zvýšenie výkonu.

    Značky:

    • php
    • pthreads
    Pridať značky

    Niekedy je potrebné vykonať niekoľko akcií súčasne, napríklad skontrolovať zmeny v jednej databázovej tabuľke a vykonať úpravy v inej. Navyše, ak jedna z operácií (napríklad kontrola zmien) trvá dlho, je zrejmé, že sekvenčné vykonávanie nezabezpečí vyváženie zdrojov.

    Na vyriešenie takýchto problémov programovanie používa multithreading - každá operácia je umiestnená v samostatnom vlákne s vyhradeným množstvom zdrojov a pracuje v ňom. S týmto prístupom budú všetky úlohy vykonávané samostatne a nezávisle.

    Hoci PHP nepodporuje multithreading, existuje niekoľko metód na jeho emuláciu, o ktorých sa bude diskutovať nižšie.

    1. Spustenie viacerých kópií skriptu – jedna kópia na operáciu

    //woman.php if (!isset($_GET["vlákno"])) ( system("wget ​​​​http://localhost/woman.php?thread=make_me_happy"); system("wget ​​​​http: //localhost/ woman.php?thread=make_me_rich"); ) elseif ($_GET["thread"] == "make_me_happy") ( make_her_happy(); ) elseif ($_GET["thread"] == "make_me_rich" ) ( nájdi_ďalší_jeden(); )

    Keď spustíme tento skript bez parametrov, automaticky spustí dve kópie seba samého s ID operácií ("thread=make_me_happy" a "thread=make_me_rich"), ktoré iniciujú požadované funkcie.

    Dosiahneme tak požadovaný výsledok - dve operácie sa vykonávajú súčasne - ale to samozrejme nie je multithreading, ale jednoducho barlička na súčasné vykonávanie úloh.

    2. Cesta Jediho – pomocou rozšírenia PCNTL

    PCNTL je rozšírenie, ktoré vám umožní plnohodnotne pracovať s procesmi. Okrem správy podporuje odosielanie správ, kontrolu stavu a nastavenie priorít. Takto vyzerá predchádzajúci skript pomocou PCNTL:

    $pid = pcntl_fork(); if ($pid == 0) ( make_her_happy(); ) elseif ($pid > 0) ( $pid2 = pcntl_fork(); if ($pid2 == 0) ( find_another_one(); ) )

    Vyzerá to dosť zmätočne, poďme riadok po riadku.

    V prvom riadku „forkujeme“ aktuálny proces (fork – kopírovanie procesu z uloženia hodnôt všetkých premenných) a rozdeľujeme ho na dva paralelne bežiace procesy (aktuálny a podradený).

    Aby sme pochopili, kde sa v podradenom alebo rodičovskom procese momentálne nachádzame, funkcia pcntl_fork vráti 0 pre dieťa a ID procesu pre rodiča. Preto sa v druhom riadku pozrieme na $pid, ak je nula, potom sme v podradenom procese - vykonáme funkciu, inak sme v rodičovi (riadok 4), potom vytvoríme ďalší proces a vykonáme úlohu rovnakým spôsobom.

    Proces vykonávania skriptu:

    Skript teda vytvorí 2 ďalšie podradené procesy, ktoré sú jeho kópiami, obsahujú rovnaké premenné s podobnými hodnotami. A pomocou identifikátora vráteného funkciou pcntl_fork sa necháme navigovať, v akom streame sa práve nachádzame a vykonáme potrebné úkony.



    Nedávno som vyskúšal pthreads a bol som milo prekvapený – ide o rozšírenie, ktoré do PHP pridáva možnosť práce s niekoľkými skutočnými vláknami. Žiadna emulácia, žiadna mágia, žiadne falzifikáty – všetko je skutočné.



    Pozerám sa na tento problém. Existuje súbor úloh, ktoré je potrebné rýchlo dokončiť. PHP má iné nástroje na riešenie tohto problému, tie tu nie sú spomenuté, článok je o pthreadoch.



    Čo je pthreads

    To je všetko! No skoro všetko. V skutočnosti je tu niečo, čo môže zvedavého čitateľa rozčúliť. Nič z toho nefunguje na štandardnom PHP skompilovanom s predvolenými možnosťami. Aby ste si užili multithreading, musíte mať vo svojom PHP povolené ZTS (Zend Thread Safety).

    Nastavenie PHP

    Ďalej PHP so ZTS. Ignorujte taký veľký rozdiel v čase vykonávania oproti PHP bez ZTS (37,65 vs 265,05 sekúnd), nesnažil som sa zovšeobecňovať nastavenie PHP. V pripade bez ZTS mam povolene XDebug napr.


    Ako vidíte, pri použití 2 vlákien je rýchlosť vykonávania programu približne 1,5-krát vyššia ako v prípade lineárneho kódu. Pri použití 4 vlákien - 3 krát.


    Môžete si všimnúť, že aj keď je procesor 8-jadrový, pri použití viac ako 4 vlákien sa doba vykonávania programu takmer nezmenila. Zdá sa, že je to spôsobené tým, že môj procesor má 4 fyzické jadrá.Pre prehľadnosť som dosku znázornil vo forme schémy.


    Zhrnutie

    PHP umožňuje celkom elegantný multithreading pomocou rozšírenia pthreads. To poskytuje citeľné zvýšenie výkonu.

    Zdá sa, že vývojári PHP zriedka používajú paralelizmus. O jednoduchosti synchrónneho kódu nebudem hovoriť, jednovláknové programovanie je samozrejme jednoduchšie a prehľadnejšie, no niekedy môže malé využitie paralelizmu priniesť citeľný nárast výkonu.

    V tomto článku sa pozrieme na to, ako je možné dosiahnuť multithreading v PHP pomocou rozšírenia pthreads. To si bude vyžadovať nainštalovanú verziu PHP 7.x ZTS (Zend Thread Safety) spolu s nainštalovaným rozšírením pthreads v3. (V čase písania tohto článku, v PHP 7.1, používatelia budú musieť nainštalovať z hlavnej vetvy v úložisku pthreads - pozri rozšírenie tretej strany.)

    Malé vysvetlenie: pthreads v2 je pre PHP 5.x a už nie je podporovaný, pthreads v3 je pre PHP 7.x a aktívne sa vyvíja.

    Po takejto odbočke poďme rovno na vec!

    Vybavovanie jednorazových úloh

    Niekedy chcete spracovať jednorazové úlohy viacvláknovým spôsobom (napríklad vykonaním nejakej I/O-viazanej úlohy). V takýchto prípadoch môžete použiť triedu Thread na vytvorenie nového vlákna a spustiť nejaké spracovanie v samostatnom vlákne.

    Napríklad:

    $task = nová trieda rozširuje vlákno ( private $response; public function run() ( $content = file_get_contents("http://google.com"); preg_match("~ (.+)~", $content, $matches); $this->response = $matches; ) ); $task->start() && $task->join(); var_dump($task->response); // reťazec (6) "Google"

    Metóda spustenia je tu naše spracovanie, ktoré sa vykoná v novom vlákne. Keď sa volá Thread::start, vytvorí sa nové vlákno a zavolá sa metóda run. Vytvorené vlákno potom pripojíme späť k hlavnému vláknu volaním Thread::join , čo bude blokovať, kým sa splodené vlákno nedokončí. To zaisťuje, že úloha sa dokončí skôr, ako sa pokúsime vydať výsledok (ktorý je uložený v $task->response).

    Nemusí byť žiaduce znečistiť triedu ďalšími zodpovednosťami súvisiacimi s logikou vlákien (vrátane zodpovednosti za definovanie metódy spustenia). Takéto triedy môžeme izolovať odvodením z triedy Threaded. Potom ich možno spustiť v inom vlákne:

    Class Task rozširuje Threaded ( public $response; public function someWork() ( $content = file_get_contents("http://google.com"); preg_match("~ (.+) ~", $content, $matches); $ this->response = $matches; ) ) $task = new Task; $thread = new class($task) extends Thread ( private $task; public function __construct(Threaded $task) ( $this->task = $task; ) public function run() ( $this->task->someWork( );)); $thread->start() && $thread->join(); var_dump($task->response);

    Akákoľvek trieda, ktorá musí byť spustená v samostatnom vlákne, musieť dediť z triedy Threaded. Je to preto, že poskytuje potrebné schopnosti na vykonávanie spracovania v rôznych vláknach, ako aj implicitné zabezpečenie a užitočné rozhrania (napríklad synchronizáciu prostriedkov).

    Pozrime sa na hierarchiu tried, ktorú poskytuje rozšírenie pthreads:

    Threaded (implementuje Traversable, Collectable) Thread Worker Volatile Pool

    Už sme prebrali a naučili sa základy tried Thread a Threaded, teraz sa pozrime na ďalšie tri (Worker , Volatile a Pool ).

    Opätovné použitie vlákna

    Spustenie nového vlákna pre každú úlohu, ktorú treba paralelizovať, je dosť drahé. Je to preto, že architektúra „nothing-common“ musí byť implementovaná v pthreadoch, aby sa v PHP dosiahlo multithreading. Čo znamená, že celý kontext vykonávania aktuálnej inštancie interpreta PHP (vrátane každej triedy, rozhrania, vlastnosti a funkcie) sa musí skopírovať pre každé vytvorené vlákno. Pretože to má výrazný vplyv na výkon, vlákno by sa malo vždy znova použiť, kedykoľvek je to možné. Vlákna je možné opätovne použiť dvoma spôsobmi: s Worker s alebo s Pool s.

    Trieda Worker sa používa na synchrónne vykonávanie série úloh v rámci iného vlákna. To sa dosiahne vytvorením novej inštancie Worker (ktorá vytvorí nové vlákno) a následným vložením úloh do zásobníka daného vlákna (pomocou Worker::stack).

    Tu je malý príklad:

    Class Task rozširuje Threaded ( private $value; public function __construct(int $i) ( $this->value = $i; ) public function run() ( usleep(250000); echo "Task: ($this->value) \n"; ) ) $worker = new Worker(); $worker->start(); for ($i = 0; $i stack(new Task($i)); ) while ($worker->collect()); $worker->shutdown();

    Vo vyššie uvedenom príklade sa 15 úloh vloží do zásobníka pre nový objekt $worker pomocou metódy Worker::stack a potom sa spracujú v poradí, v akom boli vložené. Metóda Worker::collect, ako je znázornená vyššie, sa používa na vyčistenie úloh hneď po ich dokončení. Pomocou neho vo vnútri cyklu while zablokujeme hlavné vlákno, kým nie sú dokončené všetky úlohy v zásobníku a kým nie sú vymazané - predtým, ako zavoláme Worker::shutdown . Predčasné ukončenie pracovníka (t. j. kým sú ešte úlohy, ktoré treba dokončiť) bude stále blokovať hlavné vlákno, kým sa všetky úlohy nedokončia, len sa úlohy nezhromažďujú (čo znamená úniky pamäte).

    Trieda Worker poskytuje niekoľko ďalších metód súvisiacich s jej zásobníkom úloh vrátane Worker::unstack na odstránenie poslednej vloženej úlohy a Worker::getStacked na získanie počtu úloh v zásobníku vykonávania. Zásobník pracovníka obsahuje iba úlohy, ktoré je potrebné dokončiť. Po dokončení úlohy v zásobníku sa táto odstráni a umiestni na samostatný (interný) zásobník na zber odpadu (pomocou metódy Worker::collect).

    Ďalším spôsobom, ako opätovne použiť vlákno na mnohé úlohy, je použiť fond vlákien (cez triedu Pool). Oblasť vlákien používa skupinu pracovníkov na umožnenie spúšťania úloh súčasne, v ktorom sa pri vytváraní fondu nastavuje faktor súbežnosti (počet vlákien fondu, na ktorom beží).

    Upravme vyššie uvedený príklad na použitie skupiny pracovníkov:

    Class Task rozširuje Threaded ( private $value; public function __construct(int $i) ( $this->value = $i; ) public function run() ( usleep(250000); echo "Task: ($this->value) \n"; ) ) $pool = new Pool(4); for ($i = 0; $i odoslať(nová úloha($i)); ) while ($pool->collect()); $pool->shutdown();

    Pri používaní bazéna na rozdiel od robotníka existuje niekoľko pozoruhodných rozdielov. Po prvé, fond nie je potrebné spúšťať manuálne, úlohy sa začnú vykonávať hneď, ako budú k dispozícii. Po druhé, my poslaťúlohy do bazéna, nie položte ich na stoh. Trieda Pool tiež nededí z Threaded, a preto nemôže byť odovzdaná iným vláknam (na rozdiel od Worker).

    Osvedčeným postupom je, že pracovníci a bazény by mali svoje úlohy vždy vyčistiť hneď po ich dokončení a potom ich sami manuálne ukončiť. Vlákna vytvorené pomocou triedy Thread musia byť tiež pripojené k rodičovskému vláknu.

    pthreads a (ne)mutability

    Posledná trieda, ktorej sa dotkneme, je Volatile , nový prírastok do pthreads v3. Pojem nemennosti sa v pthreadoch stal dôležitým pojmom, keďže bez neho výkon výrazne klesá. Preto sú vlastnosti Threaded tried, ktoré sú samotné Threaded objekty, teraz nemenné, a preto ich nemožno prepísať po tom, čo boli pôvodne priradené. Explicitná mutabilita pre takéto vlastnosti je v súčasnosti preferovaná a stále ju možno dosiahnuť pomocou novej triedy Volatile.

    Pozrime sa na príklad, ktorý demonštruje nové obmedzenia nemennosti:

    Class Task rozširuje Threaded // Threaded triedu ( verejná funkcia __construct() ( $this->data = new Threaded(); // $this->data nie je možné prepísať, keďže ide o Threaded vlastnosť triedy Threaded ) ) $task = new class(new Task()) rozširuje vlákno ( // trieda Threaded, pretože vlákno rozširuje verejnú funkciu Thread __construct($tm) ( $this->threadedMember = $tm; var_dump($this->threadedMember-> data); // object(Threaded)#3 (0) () $this->threadedMember = new StdClass(); // neplatná, pretože vlastnosť je Threaded členom Threaded triedy ) );

    Threaded vlastnosti tried Volatile sú na druhej strane meniteľné:

    Class Task rozširuje Volatile ( verejná funkcia __construct() ( $this->data = new Threaded(); $this->data = new StdClass(); // platné, keďže sme v nestálej triede ) ) $task = new class(new Task()) rozširuje vlákno ( verejná funkcia __construct($vm) ( $this->volatileMember = $vm; var_dump($this->volatileMember->data); // object(stdClass)#4 (0) () // stále neplatné, pretože Volatile rozširuje Threaded, takže vlastnosť je stále Threaded členom Threaded triedy $this->volatileMember = new StdClass(); ) );

    Vidíme, že trieda Volatile prepíše nemennosť uloženú rodičovskou triedou Threaded, aby poskytla možnosť meniť vlastnosti Threaded (ako aj unset() ).

    Je tu ešte jeden predmet na diskusiu na tému mutability a triedy Volatile – polia. V pthreadoch sa polia automaticky prenášajú na nestále objekty, keď sú priradené k vlastnosti triedy Threaded. Je to preto, že je jednoducho nebezpečné manipulovať s poľom z viacerých kontextov PHP.

    Pozrime sa ešte raz na príklad, aby sme lepšie porozumeli niektorým veciam:

    $pole = ; $task = new class($array) extends Thread ( private $data; public function __construct(pole $array) ( $this->data = $array; ) public function run() ( $this->data = 4; $ this->data = 5; print_r($this->data); ) ); $task->start() && $task->join(); /* Výstup: Prchavý objekt ( => 1 => 2 => 3 => 4 => 5) */

    Vidíme, že s nestálymi objektmi možno zaobchádzať, ako keby to boli polia, pretože podporujú operácie s poľami, ako je (ako je uvedené vyššie) operátor podmnožiny (). Triedy Volatile však nepodporujú základné funkcie poľa, ako napríklad array_pop a array_shift . Namiesto toho nám trieda Threaded poskytuje podobné operácie ako vstavané metódy.

    Ako ukážka:

    $data = new class extends Volatile ( public $a = 1; public $b = 2; public $c = 3; ); var_dump($data); var_dump($data->pop()); var_dump($data->shift()); var_dump($data); /* Výstup: objekt( [e-mail chránený])#1 (3) ( ["a"]=> int(1) ["b"]=> int(2) ["c"]=> int(3) ) int(3) int(1) objekt ( [e-mail chránený])#1 (1) ( ["b"]=> int(2) ) */

    Medzi ďalšie podporované operácie patrí Threaded::chunk a Threaded::merge .

    Synchronizácia

    V poslednej časti tohto článku sa pozrieme na synchronizáciu v pthreadoch. Synchronizácia je metóda, ktorá vám umožňuje kontrolovať prístup k zdieľaným zdrojom.

    Implementujme napríklad jednoduché počítadlo:

    $counter = new class extends Thread ( public $i = 0; public function run() ( for ($i = 0; $i i; ) ) ); $counter->start(); for ($i = 0; $i i; ) $counter->join(); var_dump($counter->i); // vypíše číslo od 10 do 20

    Bez použitia synchronizácie nie je výstup deterministický. Viaceré vlákna zapisujú do rovnakej premennej bez riadeného prístupu, čo znamená, že aktualizácie sa stratia.

    Opravme to, aby sme dostali správny výstup 20 pridaním synchronizácie:

    $counter = new class extends Thread ( public $i = 0; public function run() ( $this->synchronized(function () ( for ($i = 0; $i i; ) )); ) ); $counter->start(); $counter->synchronized(funkcia ($counter) ( for ($i = 0; $i i; ) ), $counter); $counter->join(); var_dump($counter->i); // int(20)

    Synchronizované bloky kódu môžu medzi sebou komunikovať aj pomocou metód Threaded::wait a Threaded::notify (alebo Threaded::notifyAll).

    Tu je alternatívny prírastok v dvoch synchronizovaných slučkách while:

    $counter = nová trieda rozširuje vlákno ( public $cond = 1; verejná funkcia run() ( $this->synchronized(function () ( for ($i = 0; $i notify(); if ($this->cond) === 1) ( $this->cond = 2; $this->čakaj(); ) ) )); ) ); $counter->start(); $counter->synchronized(function ($counter) ( if ($counter->cond !== 2) ( $counter->wait(); // počkajte, kým druhý začne prvý ) pre ($i = 10; $i notify(); if ($counter->cond === 2) ( $counter->cond = 1; $counter->wait(); ) ) ), $counter); $counter->join(); /* Výstup: int(0) int(10) int(1) int(11) int(2) int(12) int(3) int(13) int(4) int(14) int(5) int( 15) int(6) int(16) int(7) int(17) int(8) int(18) int(9) int(19) */

    Môžete si všimnúť ďalšie podmienky, ktoré sa týkajú volania Threaded::wait . Tieto podmienky sú kritické, pretože umožňujú obnovenie synchronizovaného spätného volania, keď dostane upozornenie a zadaná podmienka je pravdivá. Je to dôležité, pretože upozornenia môžu prichádzať z iných miest, než keď sa volá Threaded::notify. Ak by teda volania metódy Threaded::wait neboli zabalené do podmienok, vykonali by sme falošné budíky, čo povedie k nepredvídateľnému správaniu kódu.

    Záver

    Pozreli sme sa na päť tried v balíku pthreads (Threaded , Thread , Worker , Volatile a Pool) a ako sa jednotlivé triedy používajú. Pozreli sme sa aj na nový koncept immutability v pthreads, urobili sme krátky prehľad podporovaných synchronizačných funkcií. S týmito základmi sa teraz môžeme začať zaoberať tým, ako používať pthreads v skutočných prípadoch! Toto bude téma nášho ďalšieho príspevku.

    Ak máte záujem o preklad ďalšieho príspevku, dajte mi vedieť: komentujte na sociálnej sieti. siete, hlasujte za a zdieľajte príspevok s kolegami a priateľmi.

    Nedávno som vyskúšal pthreads a bol som milo prekvapený – ide o rozšírenie, ktoré do PHP pridáva možnosť práce s niekoľkými skutočnými vláknami. Žiadna emulácia, žiadna mágia, žiadne falzifikáty – všetko je skutočné.



    Pozerám sa na tento problém. Existuje súbor úloh, ktoré je potrebné rýchlo dokončiť. PHP má iné nástroje na riešenie tohto problému, tie tu nie sú spomenuté, článok je o pthreadoch.



    Čo je pthreads

    To je všetko! No skoro všetko. V skutočnosti je tu niečo, čo môže zvedavého čitateľa rozčúliť. Nič z toho nefunguje na štandardnom PHP skompilovanom s predvolenými možnosťami. Aby ste si užili multithreading, musíte mať vo svojom PHP povolené ZTS (Zend Thread Safety).

    Nastavenie PHP

    Ďalej PHP so ZTS. Ignorujte taký veľký rozdiel v čase vykonávania oproti PHP bez ZTS (37,65 vs 265,05 sekúnd), nesnažil som sa zovšeobecňovať nastavenie PHP. V pripade bez ZTS mam povolene XDebug napr.


    Ako vidíte, pri použití 2 vlákien je rýchlosť vykonávania programu približne 1,5-krát vyššia ako v prípade lineárneho kódu. Pri použití 4 vlákien - 3 krát.


    Môžete si všimnúť, že aj keď je procesor 8-jadrový, pri použití viac ako 4 vlákien sa doba vykonávania programu takmer nezmenila. Zdá sa, že je to spôsobené tým, že môj procesor má 4 fyzické jadrá.Pre prehľadnosť som dosku znázornil vo forme schémy.


    Zhrnutie

    PHP umožňuje celkom elegantný multithreading pomocou rozšírenia pthreads. To poskytuje citeľné zvýšenie výkonu.

    Štítky: Pridajte štítky