Viacvláknové programovanie v PHP pomocou prekladu Pthreads. Viacvláknové výpočty v PHP: pthreads Path of the Jedi - pomocou rozšírenia PCNTL

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



Zvažujem takúto úlohu. 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 sú 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 zapnuté ZTS (Zend Thread Safety).

Nastavenie PHP

Ďalej PHP so ZTS. Nevšímajte si taký veľký rozdiel v dobe vykonávania oproti PHP bez ZTS (37,65 vs 265,05 sekúnd), nesnažil som sa to zredukovať na spoločného menovateľa Nastavenia 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ý, čas vykonávania programu zostal takmer nezmenený, ak sa použili viac ako 4 vlákna. 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

V PHP sa dá celkom elegantne pracovať s multithreadingom pomocou rozšírenia pthreads. To vedie k výraznému zvýšeniu produktivity.

Štítky: Pridajte štítky

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) zaberie veľa času, je zrejmé, že sekvenčné vykonávanie nezabezpečí vyváženie zdrojov.

Na vyriešenie tohto druhu problému programovanie používa multithreading - každá operácia je umiestnená v samostatnom vlákne s prideleným množstvom zdrojov a pracuje v ňom. S týmto prístupom budú všetky úlohy dokončené samostatne a nezávisle.

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

1. Spustenie niekoľký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ú vykonávanie potrebných funkcií.

Týmto spôsobom dosiahneme 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 používajúci 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ť neprehľadne, poďme si to prejsť rad za radom.

V prvom riadku „forkujeme“ aktuálny proces (fork kopíruje proces pri zachovaní 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 nachádzame tento moment, v procese dieťaťa alebo matky funkcia pcntl_fork vráti 0 pre dieťa a ID procesu pre matku. Preto sa v druhom riadku pozrieme na $pid, ak je nula, potom sme v podradenom procese - vykonávame funkciu, inak sme v matke (riadok 4), potom vytvoríme ďalší proces a podobne vykonajte úlohu.

Proces vykonávania skriptu:

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

Zdá sa, PHP vývojári paralelizmus sa používa zriedka. Nebudem hovoriť o jednoduchosti synchrónneho kódu, 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. Na to budete potrebovať nainštalovanú verziu PHP 7.x ZTS (Zend Thread Safety) spolu s nainštalované rozšírenie 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é upresnenie: pthreads v2 je určený 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 k veci!

Spracovanie 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. Potom pripojíme podriadené vlákno späť k hlavnému vláknu volaním Thread::join , ktoré sa zablokuje, kým sa podriadené vlákno nedokončí. To zaisťuje, že úloha sa dokončí skôr, ako sa pokúsime vytlačiť výsledok (ktorý je uložený v $task->response).

Nemusí byť žiaduce znečistiť triedu ďalšími zodpovednosťami spojenými s logikou toku (vrátane zodpovednosti za definovanie metódy behu). Takéto triedy môžeme rozlíšiť tak, že ich zdedíme od 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);

Každá trieda, ktorú je potrebné spustiť 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ú ponúka 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ákien

Spustenie nového vlákna pre každú úlohu, ktorú treba paralelizovať, je dosť drahé. Je to preto, že architektúra common-nothing 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) musí byť skopírovaný pre každé vytvorené vlákno. Pretože to má výrazný vplyv na výkon, stream by sa mal vždy znova použiť, kedykoľvek je to možné. Vlákna možno opätovne použiť dvoma spôsobmi: pomocou pracovníkov alebo pomocou skupín.

Trieda Worker sa používa na synchrónne vykonávanie množstva ú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 tohto samostatné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 pre nový objekt $worker vloží do zásobníka 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 dokončení. Pomocou neho v rámci cyklu while zablokujeme hlavné vlákno, kým sa nedokončia a nevyčistia všetky úlohy v zásobníku - predtým, ako zavoláme Worker::shutdown . Predčasné ukončenie pracovníka (t. j. kým ešte existujú úlohy, ktoré je potrebné dokončiť) bude stále blokovať hlavné vlákno, kým sa všetky úlohy nedokončia, akurát, že úlohy nebudú zhromažďované (č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 ulož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é vykonať. 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 opätovného použitia vlákna vo viacerých úlohách je použitie fondu vlákien (cez triedu Pool). Oblasť vlákien používa skupinu pracovníkov na umožnenie vykonávania úloh súčasne, v ktorom je faktor súbežnosti (počet vlákien fondu, s ktorými pracuje) nastavený pri vytváraní fondu.

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 navyše nededí z Threaded, a preto nemôže byť odovzdaná iným vláknam (na rozdiel od Worker).

Ako dobre cvicenie Pre pracovníkov a bazény by ste mali vždy vyčistiť ich úlohy 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)mutabilita

Posledná trieda, ktorej sa dotkneme, je Volatile, nový prírastok do pthreads v3. Nemennosť sa stala dôležitým pojmom v pthreadoch, pretože bez nej výrazne trpí výkon. Preto sú vlastnosti Threaded tried, ktoré sú samotné Threaded objekty, štandardne nemenné, a preto ich nemožno po počiatočnom priradení prepísať. Explicitná mutabilita pre takéto vlastnosti je v súčasnosti preferovaná a stále sa dá 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 ) );

Na druhej strane závitové vlastnosti tried Volatile sú 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 má prednosť pred nemennosťou, ktorú vnucuje nadradená trieda Threaded, aby poskytovala možnosť meniť vlastnosti Threaded (ako aj unset()).

Existuje ďalší predmet diskusie na tému premenlivosti 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 jednoducho nie je bezpečné manipulovať s radom viacerých kontextov PHP.

Pozrime sa znova 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 (). Volatile triedy však nepodporujú základné funkcie poľa, ako napríklad array_pop a array_shift. Namiesto toho nám trieda Threaded poskytuje také 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(trieda@anonymný)#1 (3) ( ["a"]=> int(1) ["b"]=> int(2) ["c"]=> int(3) ) int(3) int(1) objekt(trieda@anonymný)#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); // vytlačí čí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 tak, aby sme dostali správny výstup 20 pridaním časovania:

$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 pochádzať z iných miest, než keď sa volá Threaded::notify. Ak teda volania metódy Threaded::wait neboli uzavreté v podmienkach, vykonáme ich falošné budíky, čo povedie k nepredvídateľnému správaniu kódu.

Záver

Pozreli sme sa na päť tried balíka pthreads (Threaded, Thread, Worker, Volatile a Pool) a ako sa jednotlivé triedy používajú. Pozreli sme sa aj na nový koncept immutability v pthreads a poskytli sme stručný prehľad podporovaných možností synchronizácie. S týmito základmi sa teraz môžeme začať zaoberať tým, ako možno pthreads použiť 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álnych sieťach. siete, hlasujte za a zdieľajte príspevok s kolegami a priateľmi.

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



    Zvažujem takúto úlohu. 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 sú 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 zapnuté ZTS (Zend Thread Safety).

    Nastavenie PHP

    Ďalej PHP so ZTS. Nevadí veľký rozdiel v dobe 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ý, čas vykonávania programu zostal takmer nezmenený, ak sa použili viac ako 4 vlákna. 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

    V PHP sa dá celkom elegantne pracovať s multithreadingom pomocou rozšírenia pthreads. To vedie k výraznému zvýšeniu produktivity.

    Značky:

    • php
    • pthreads
    Pridať značky

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



    Zvažujem takúto úlohu. 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 sú 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 zapnuté ZTS (Zend Thread Safety).

    Nastavenie PHP

    Ďalej PHP so ZTS. Nevadí veľký rozdiel v dobe 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ý, čas vykonávania programu zostal takmer nezmenený, ak sa použili viac ako 4 vlákna. 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

    V PHP sa dá celkom elegantne pracovať s multithreadingom pomocou rozšírenia pthreads. To vedie k výraznému zvýšeniu produktivity.



    
    Hore