Amorské diskusní vlákno php. Vícevláknové výpočty v PHP: pthreads

Někdy je nutné provést několik akcí současně, například zkontrolovat změny v jedné databázové tabulce a provést úpravy jiné. Navíc, pokud jedna z operací (například kontrola změn) zabere hodně času, je zřejmé, že sekvenční provádění nezajistí vyvážení zdrojů.

K vyřešení tohoto druhu problému používá programování multithreading – každá operace je umístěna do samostatného vlákna s přiděleným množstvím zdrojů a pracuje v něm. S tímto přístupem budou všechny úkoly dokončeny samostatně a nezávisle.

Přestože PHP nepodporuje multithreading, existuje několik metod pro jeho emulaci, o kterých bude pojednáno níže.

1. Spuštění několika kopií skriptu – jedna kopie na operaci

//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" ) ( najdi_dalsi_jeden( ; )

Když tento skript spustíme bez parametrů, automaticky spustí dvě své kopie s ID operací ("thread=make_me_happy" a "thread=make_me_rich"), které zahájí provádění nezbytných funkcí.

Tímto způsobem dosáhneme požadovaného výsledku - jsou prováděny dvě operace současně - ale to samozřejmě není multithreading, ale prostě berlička pro současné provádění úkolů.

2. Cesta Jediho – pomocí rozšíření PCNTL

PCNTL je rozšíření, které umožňuje plnohodnotnou práci s procesy. Kromě správy podporuje odesílání zpráv, kontrolu stavu a nastavení priorit. Takto vypadá předchozí skript používající PCNTL:

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

Vypadá to dost zmateně, pojďme si to projít řádek po řádku.

V prvním řádku „forkujeme“ aktuální proces (fork kopíruje proces při zachování hodnot všech proměnných) a rozdělujeme ho na dva paralelně běžící procesy (aktuální a podřízený).

Abychom pochopili, zda se aktuálně nacházíme v procesu dítěte nebo matky, funkce pcntl_fork vrací 0 pro dítě a ID procesu pro matku. Proto se na druhém řádku podíváme na $pid, pokud je nula, pak jsme v podřízeném procesu - vykonáváme funkci, jinak jsme v matce (řádek 4), pak vytvoříme další proces a obdobně proveďte úkol.

Proces provádění skriptu:

Skript tedy vytvoří další 2 podřízené procesy, které jsou jeho kopiemi a obsahují stejné proměnné s podobnými hodnotami. A pomocí identifikátoru vráceného funkcí pcntl_fork zjistíme, ve kterém vláknu se právě nacházíme a provedeme potřebné akce.



Nedávno jsem vyzkoušel pthreads a byl jsem mile překvapen – je to rozšíření, které přidává možnost pracovat s více reálnými vlákny v PHP. Žádná emulace, žádná magie, žádné padělky – vše je skutečné.



Uvažuji o takovém úkolu. Existuje skupina úkolů, které je třeba rychle splnit. PHP má další nástroje pro řešení tohoto problému, ty zde nejsou uvedeny, článek je o pthreadech.



Co jsou pthreads

To je vše! Tedy skoro všechno. Ve skutečnosti je tu něco, co může zvídavého čtenáře naštvat. Nic z toho nefunguje na standardním PHP zkompilovaném s výchozími možnostmi. Abyste si mohli užívat multithreading, musíte mít v PHP povoleno ZTS (Zend Thread Safety).

Nastavení PHP

Dále PHP se ZTS. Nevadí velký rozdíl v době provádění oproti PHP bez ZTS (37,65 vs 265,05 sekund), nezkoušel jsem nastavení PHP zobecňovat. V případě bez ZTS mám povoleno například XDebug.


Jak vidíte, při použití 2 vláken je rychlost provádění programu přibližně 1,5x vyšší než v případě lineárního kódu. Při použití 4 nití - 3krát.


Můžete si všimnout, že i když je procesor 8jádrový, doba provádění programu zůstala téměř nezměněna, pokud bylo použito více než 4 vlákna. Zdá se, že je to způsobeno tím, že můj procesor má 4 fyzická jádra Pro přehlednost jsem desku znázornil ve formě schématu.


souhrn

V PHP lze celkem elegantně pracovat s multithreadingem pomocí rozšíření pthreads. To poskytuje znatelné zvýšení produktivity.

Nedávno jsem vyzkoušel pthreads a byl jsem mile překvapen – je to rozšíření, které přidává možnost pracovat s více reálnými vlákny v PHP. Žádná emulace, žádná magie, žádné padělky – vše je skutečné.



Uvažuji o takovém úkolu. Existuje skupina úkolů, které je třeba rychle splnit. PHP má další nástroje pro řešení tohoto problému, ty zde nejsou uvedeny, článek je o pthreadech.



Co jsou pthreads

To je vše! Tedy skoro všechno. Ve skutečnosti je tu něco, co může zvídavého čtenáře naštvat. Nic z toho nefunguje na standardním PHP zkompilovaném s výchozími možnostmi. Abyste si mohli užívat multithreading, musíte mít v PHP povoleno ZTS (Zend Thread Safety).

Nastavení PHP

Dále PHP se ZTS. Nevadí velký rozdíl v době provádění oproti PHP bez ZTS (37,65 vs 265,05 sekund), nezkoušel jsem nastavení PHP zobecňovat. V případě bez ZTS mám povoleno například XDebug.


Jak vidíte, při použití 2 vláken je rychlost provádění programu přibližně 1,5x vyšší než v případě lineárního kódu. Při použití 4 nití - 3krát.


Můžete si všimnout, že i když je procesor 8jádrový, doba provádění programu zůstala téměř nezměněna, pokud bylo použito více než 4 vlákna. Zdá se, že je to způsobeno tím, že můj procesor má 4 fyzická jádra Pro přehlednost jsem desku znázornil ve formě schématu.


souhrn

V PHP lze celkem elegantně pracovat s multithreadingem pomocí rozšíření pthreads. To poskytuje znatelné zvýšení produktivity.

Štítky: Přidat štítky

  • programování,
  • Paralelní programování
  • Nedávno jsem vyzkoušel pthreads a byl jsem mile překvapen – je to rozšíření, které přidává možnost pracovat s více reálnými vlákny v PHP. Žádná emulace, žádná magie, žádné padělky – vše je skutečné.



    Uvažuji o takovém úkolu. Existuje skupina úkolů, které je třeba rychle splnit. PHP má další nástroje pro řešení tohoto problému, ty zde nejsou uvedeny, článek je o pthreadech.



    Co jsou pthreads

    To je vše! Tedy skoro všechno. Ve skutečnosti je tu něco, co může zvídavého čtenáře naštvat. Nic z toho nefunguje na standardním PHP zkompilovaném s výchozími možnostmi. Abyste si mohli užívat multithreading, musíte mít v PHP povoleno ZTS (Zend Thread Safety).

    Nastavení PHP

    Dále PHP se ZTS. Nevadí velký rozdíl v době provádění oproti PHP bez ZTS (37,65 vs 265,05 sekund), nezkoušel jsem nastavení PHP zobecňovat. V případě bez ZTS mám povoleno například XDebug.


    Jak vidíte, při použití 2 vláken je rychlost provádění programu přibližně 1,5x vyšší než v případě lineárního kódu. Při použití 4 nití - 3krát.


    Můžete si všimnout, že i když je procesor 8jádrový, doba provádění programu zůstala téměř nezměněna, pokud bylo použito více než 4 vlákna. Zdá se, že je to způsobeno tím, že můj procesor má 4 fyzická jádra Pro přehlednost jsem desku znázornil ve formě schématu.


    souhrn

    V PHP lze celkem elegantně pracovat s multithreadingem pomocí rozšíření pthreads. To poskytuje znatelné zvýšení produktivity.

    Štítky:

    • php
    • pthreads
    Přidat štítky

    Zdá se, že vývojáři PHP zřídka používají souběžnost. Nebudu mluvit o jednoduchosti synchronního kódu; jednovláknové programování je samozřejmě jednodušší a přehlednější, ale občas může malé využití paralelismu přinést citelný nárůst výkonu.

    V tomto článku se podíváme na to, jak lze v PHP dosáhnout multithreadingu pomocí rozšíření pthreads. To bude vyžadovat instalaci ZTS (Zend Thread Safety) verze PHP 7.x spolu s nainstalovaným rozšířením pthreads v3. (V době psaní tohoto článku budou v PHP 7.1 uživatelé muset instalovat z hlavní větve v úložišti pthreads – viz rozšíření třetí strany.)

    Malé upřesnění: pthreads v2 je určen pro PHP 5.x a již není podporován, pthreads v3 je pro PHP 7.x a aktivně se vyvíjí.

    Po takové odbočce přejděme rovnou k věci!

    Zpracování jednorázových úkolů

    Někdy chcete zpracovávat jednorázové úlohy vícevláknovým způsobem (například provedení nějaké I/O vázané úlohy). V takových případech můžete pomocí třídy Thread vytvořit nové vlákno a spustit nějaké zpracování v samostatném vláknu.

    Například:

    $task = nová třída rozšiřuje 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); // řetězec (6) "Google"

    Zde je metodou run naše zpracování, které bude provedeno v novém vlákně. Když se zavolá Thread::start, vytvoří se nové vlákno a zavolá se metoda run. Potom připojíme podřízené vlákno zpět k hlavnímu vláknu voláním Thread::join , které se zablokuje, dokud se podřízené vlákno nedokončí. Tím je zajištěno, že se úloha dokončí dříve, než se pokusíme vytisknout výsledek (který je uložen v $task->response).

    Nemusí být žádoucí znečišťovat třídu dalšími odpovědnostmi spojenými s logikou toku (včetně odpovědnosti za definování metody běhu). Takové třídy můžeme rozlišit tak, že je zdědíme od třídy Threaded. Poté je lze spustit v jiném vlákně:

    Class Task rozšiřuje Threaded ( public $response; public function someWork() ( $content = file_get_contents("http://google.com"); preg_match("~ (.+) ~", $content, $matches); $ this->response = $shoduje se ) ) $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);

    Jakákoli třída, která musí být spuštěna v samostatném vláknu musí dědit z třídy Threaded. Je to proto, že poskytuje potřebné schopnosti pro provádění zpracování na různých vláknech, stejně jako implicitní zabezpečení a užitečná rozhraní (jako je synchronizace prostředků).

    Podívejme se na hierarchii tříd, kterou nabízí rozšíření pthreads:

    Threaded (implementuje Traversable, Collectable) Thread Worker Volatile Pool

    Základy tříd Thread a Threaded jsme již probrali a naučili se, nyní se podíváme na další tři (Worker, Volatile a Pool).

    Opětovné použití vláken

    Založení nového vlákna pro každou úlohu, kterou je třeba paralelizovat, je poměrně drahé. Je to proto, že architektura common-nothing musí být implementována v pthreadech, aby bylo dosaženo multithreadingu v PHP. Což znamená, že celý kontext provádění aktuální instance interpretu PHP (včetně každé třídy, rozhraní, vlastnosti a funkce) musí být zkopírován pro každé vytvořené vlákno. Protože to má znatelný dopad na výkon, měl by být stream vždy znovu použit, kdykoli je to možné. Vlákna lze znovu použít dvěma způsoby: pomocí Workers nebo pomocí Pools.

    Třída Worker se používá k provádění řady úloh synchronně v rámci jiného vlákna. Toho se dosáhne vytvořením nové instance Worker (která vytvoří nové vlákno) a poté odesláním úkolů do zásobníku tohoto samostatného vlákna (pomocí Worker::stack).

    Zde je malý příklad:

    Class Task rozšiřuje 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();

    Ve výše uvedeném příkladu je 15 úloh pro nový objekt $worker vloženo do zásobníku pomocí metody Worker::stack a poté jsou zpracovány v pořadí, v jakém byly vloženy. Metoda Worker::collect, jak je znázorněna výše, se používá k vyčištění úkolů, jakmile skončí jejich provádění. S ním, uvnitř smyčky while, zablokujeme hlavní vlákno, dokud nejsou dokončeny a vymazány všechny úkoly v zásobníku - předtím, než zavoláme Worker::shutdown . Předčasné ukončení pracovníka (tj. dokud jsou ještě úkoly, které je třeba dokončit) bude stále blokovat hlavní vlákno, dokud všechny úkoly nedokončí své provedení, pouze úkoly nebudou shromažďovány (což znamená úniky paměti).

    Třída Worker poskytuje několik dalších metod souvisejících s jejím zásobníkem úloh, včetně Worker::unstack pro odstranění posledního skládaného úkolu a Worker::getStacked pro získání počtu úkolů v zásobníku provádění. Zásobník pracovníka obsahuje pouze úkoly, které je třeba provést. Jakmile je úkol na zásobníku dokončen, je odstraněn a umístěn na samostatný (interní) zásobník pro sběr odpadu (pomocí metody Worker::collect).

    Dalším způsobem, jak znovu použít vlákno ve více úlohách, je použití fondu vláken (prostřednictvím třídy Pool). Fond vláken používá skupinu pracovníků k umožnění provádění úloh zároveň, ve kterém je faktor souběžnosti (počet vláken fondu, se kterými pracuje) nastaven při vytvoření fondu.

    Upravme výše uvedený příklad pro použití skupiny pracovníků:

    Class Task rozšiřuje 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 submit(new Task($i)); ) while ($pool->collect()); $pool->shutdown();

    Při použití bazénu na rozdíl od pracovníka existuje několik významných rozdílů. Za prvé, fond není nutné spouštět ručně, začne provádět úkoly, jakmile budou k dispozici. Za druhé, my poslatúkoly do bazénu, ne dát je na hromádku. Třída Pool navíc nedědí z Threaded, a proto nemůže být předána jiným vláknům (na rozdíl od Worker).

    Pro pracovníky a fondy je dobrým zvykem vždy uklidit své úkoly, jakmile je dokončí, a poté je sami ručně ukončit. Vlákna vytvořená pomocí třídy Thread musí být také připojena k nadřazenému vláknu.

    pthreads a (ne)mutabilita

    Poslední třídou, které se dotkneme, je Volatile, nový přírůstek do pthreads v3. Neměnnost se stala důležitým pojmem v pthreadech, protože bez ní výkon výrazně trpí. Proto jsou ve výchozím nastavení vlastnosti Threaded tříd, které jsou samy Threaded objekty, nyní neměnné, a proto je nelze po jejich počátečním přiřazení přepsat. Explicitní mutabilita pro takové vlastnosti je v současné době preferována a lze ji stále dosáhnout pomocí nové třídy Volatile.

    Podívejme se na příklad, který demonstruje nová omezení neměnnosti:

    Class Task rozšiřuje Threaded // Threaded třídu ( veřejná funkce __construct() ( $this->data = new Threaded(); // $this->data nelze přepsat, protože jde o vlastnost Threaded třídy Threaded ) ) $task = new class(new Task()) rozšiřuje vlákno ( // třída Threaded, protože vlákno rozšiřuje veřejnou funkci Threaded __construct($tm) ( $this->threadedMember = $tm; var_dump($this->threadedMember-> data // object(Threaded)#3 (0) () $this->threadedMember = new StdClass( // neplatný, protože vlastnost je Threaded člen třídy Threaded ) );

    Na druhé straně závitové vlastnosti tříd Volatile jsou proměnlivé:

    Class Task rozšiřuje Volatile ( veřejná funkce __construct() ( $this->data = new Threaded(); $this->data = new StdClass(); // platné, protože jsme v nestálé třídě) ) $task = new class(new Task()) rozšiřuje vlákno ( veřejná funkce __construct($vm) ( $this->volatileMember = $vm; var_dump($this->volatileMember->data); // object(stdClass)#4 (0) () // stále neplatné, protože Volatile rozšiřuje Threaded, takže vlastnost je stále Threaded členem Threaded třídy $this->volatileMember = new StdClass() );

    Vidíme, že třída Volatile potlačuje neměnnost, kterou ukládá její nadřazená třída Threaded, aby poskytovala možnost měnit vlastnosti Threaded (stejně jako je unset()).

    Tématem proměnlivosti a třídy Volatile je ještě jeden předmět diskuse – pole. V pthreadech jsou pole automaticky přetypována na objekty Volatile, když jsou přiřazena k vlastnosti třídy Threaded. Je to proto, že není bezpečné manipulovat s polem více kontextů PHP.

    Podívejme se znovu na příklad, abychom některým věcem lépe porozuměli:

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

    Vidíme, že s nestálými objekty lze zacházet, jako by to byly pole, protože podporují operace s poli, jako je (jak je ukázáno výše) operátor subset(). Volatile třídy však nepodporují základní funkce pole, jako je array_pop a array_shift. Místo toho nám třída Threaded poskytuje takové operace jako vestavěné metody.

    Jako ukázka:

    $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(třída@anonymní)#1 (3) ( ["a"]=> int(1) ["b"]=> int(2) ["c"]=> int(3) ) int(3) int(1) objekt(třída@anonymní)#1 (1) ( ["b"]=> int(2) ) */

    Mezi další podporované operace patří Threaded::chunk a Threaded::merge .

    Synchronizace

    V poslední části tohoto článku se podíváme na synchronizaci v pthreadech. Synchronizace je metoda, která umožňuje řídit přístup ke sdíleným zdrojům.

    Implementujme například jednoduchý čítač:

    $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); // vytiskne číslo od 10 do 20

    Bez použití synchronizace není výstup deterministický. Více vláken zapisuje do stejné proměnné bez řízeného přístupu, což znamená, že aktualizace budou ztraceny.

    Opravme to, abychom získali správný výstup 20 přidáním časování:

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

    Synchronizované bloky kódu mohou také mezi sebou komunikovat pomocí metod Threaded::wait a Threaded::notify (nebo Threaded::notifyAll).

    Zde je alternativní přírůstek ve dvou synchronizovaných smyčkách while:

    $counter = nová třída rozšiřuje vlákno ( public $cond = 1; veřejná funkce run() ( $this->synchronized(function () ( for ($i = 0; $i notify(); if ($this->cond) === 1) ( $this->cond = 2; $this->wait(); ) ) )); $counter->start(); $counter->synchronized(funkce ($counter) ( if ($counter->cond !== 2) ( $counter->wait(); // počkejte, až začne druhý jako první ) pro ($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šimnout dalších podmínek, které byly umístěny kolem volání Threaded::wait . Tyto podmínky jsou kritické, protože umožňují obnovení synchronizovaného zpětného volání, když obdrží oznámení a zadaná podmínka je pravdivá. To je důležité, protože oznámení mohou přicházet z jiných míst, než když se volá Threaded::notify. Pokud tedy volání metody Threaded::wait nebyla uzavřena v podmínkách, provedeme falešné probuzení, což povede k nepředvídatelnému chování kódu.

    Závěr

    Podívali jsme se na pět tříd balíčku pthreads (Threaded, Thread, Worker, Volatile a Pool) a na to, jak se jednotlivé třídy používají. Podívali jsme se také na nový koncept neměnnosti v pthreadech a poskytli stručný přehled podporovaných možností synchronizace. S těmito základy se nyní můžeme začít zabývat tím, jak lze pthreads použít v reálných případech! To bude tématem našeho dalšího příspěvku.

    Pokud máte zájem o překlad dalšího příspěvku, dejte mi vědět: komentujte na sociálních sítích. sítě, hlasujte pro a sdílejte příspěvek s kolegy a přáteli.