Programare multi-threaded în PHP folosind Pthreads Translation. Calcul multi-threaded în PHP: pthreads Path of the Jedi - folosind extensia PCNTL

Am încercat recent pthreads și am fost plăcut surprins - este o extensie care adaugă capacitatea de a lucra cu mai multe fire reale în PHP. Fără emulare, fără magie, fără falsuri - totul este real.



Mă gândesc la o astfel de sarcină. Există o serie de sarcini care trebuie finalizate rapid. PHP are alte instrumente pentru rezolvarea acestei probleme, ele nu sunt menționate aici, articolul este despre pthreads.



Ce sunt pthread-urile

Asta e tot! Ei bine, aproape totul. De fapt, există ceva care poate supăra un cititor curios. Nimic din acestea nu funcționează pe PHP standard compilat cu opțiuni implicite. Pentru a vă bucura de multithreading, trebuie să aveți ZTS (Zend Thread Safety) activat în PHP.

Configurare PHP

Apoi, PHP cu ZTS. Nu acordați atenție unei diferențe atât de mari de timp de execuție față de PHP fără ZTS (37,65 vs 265,05 secunde), nu am încercat să o reduc la un numitor comun setări PHP. În cazul fără ZTS, am XDebug activat, de exemplu.


După cum puteți vedea, atunci când utilizați 2 fire, viteza de execuție a programului este de aproximativ 1,5 ori mai mare decât în ​​cazul codului liniar. Când utilizați 4 fire - de 3 ori.


Puteți observa că, deși procesorul este cu 8 nuclee, timpul de execuție al programului a rămas aproape neschimbat dacă s-au folosit mai mult de 4 fire. Se pare că acest lucru se datorează faptului că procesorul meu are 4 nuclee fizice.Pentru claritate, am descris placa sub forma unei diagrame.


rezumat

În PHP, este posibil să lucrați destul de elegant cu multithreading folosind extensia pthreads. Acest lucru oferă o creștere vizibilă a productivității.

Etichete: Adăugați etichete

Uneori devine necesar să se efectueze mai multe acțiuni simultan, de exemplu, verificarea modificărilor dintr-un tabel al bazei de date și efectuarea modificărilor la altul. Mai mult, dacă una dintre operații (de exemplu, verificarea modificărilor) durează mult, este evident că execuția secvențială nu va asigura echilibrarea resurselor.

Pentru a rezolva acest tip de problemă, programarea folosește multithreading - fiecare operație este plasată într-un fir separat cu o cantitate alocată de resurse și funcționează în cadrul acestuia. Cu această abordare, toate sarcinile vor fi îndeplinite separat și independent.

Deși PHP nu acceptă multithreading, există mai multe metode de emulare, despre care vom vorbi de mai jos.

1. Rularea mai multor copii ale scriptului - o copie per operație

//woman.php if (!isset($_GET["thread"])) ( 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" ) (găsește_altul_unul(); )

Când executăm acest script fără parametri, rulează automat două copii ale lui, cu ID-uri de operație ("thread=make_me_happy" și "thread=make_me_rich"), care inițiază execuția funcțiilor necesare.

Astfel obținem rezultatul dorit - două operații sunt efectuate simultan - dar acesta, desigur, nu este multithreading, ci pur și simplu o cârjă pentru îndeplinirea sarcinilor simultan.

2. Calea Jedi - folosind extensia PCNTL

PCNTL este o extensie care vă permite să lucrați pe deplin cu procesele. Pe lângă management, acceptă trimiterea de mesaje, verificarea stării și stabilirea priorităților. Iată cum arată scriptul anterior folosind PCNTL:

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

Pare destul de confuz, hai să trecem prin el rând cu rând.

În prima linie, „furcăm” procesul curent (furcătura este copierea unui proces, păstrând în același timp valorile tuturor variabilelor), împărțindu-l în două procese (curent și copil) care rulează în paralel.

Pentru a înțelege unde suntem acest moment, într-un proces copil sau mamă, funcția pcntl_fork returnează 0 pentru copil și ID-ul procesului pentru mamă. Prin urmare, în a doua linie, ne uităm la $pid, dacă este zero, atunci suntem în procesul copil - executăm funcția, în caz contrar, suntem în mamă (linia 4), apoi creăm un alt proces și efectuează în mod similar sarcina.

Procesul de executare a scriptului:

Astfel, scriptul creează încă 2 procese copil, care sunt copiile sale și conțin aceleași variabile cu valori similare. Și folosind identificatorul returnat de funcția pcntl_fork, aflăm în ce fir ne aflăm în prezent și efectuăm acțiunile necesare.

Se pare, Dezvoltatori PHP paralelismul este rar folosit. Nu voi vorbi despre simplitatea codului sincron; programarea cu un singur thread este, desigur, mai simplă și mai clară, dar uneori o mică utilizare a paralelismului poate aduce o creștere vizibilă a performanței.

În acest articol, vom arunca o privire asupra modului în care multithreading poate fi realizat în PHP folosind extensia pthreads. Pentru a face acest lucru, veți avea nevoie de versiunea ZTS (Zend Thread Safety) a PHP 7.x instalată, împreună cu extensia instalată pthreads v3. (La momentul scrierii, în PHP 7.1, utilizatorii vor trebui să instaleze din ramura principală din depozitul pthreads - vezi extensia terță parte.)

O mică precizare: pthreads v2 este destinat PHP 5.x și nu mai este acceptat, pthreads v3 este pentru PHP 7.x și este în curs de dezvoltare.

După o astfel de digresiune, să trecem direct la subiect!

Prelucrarea sarcinilor unice

Uneori doriți să procesați sarcini unice într-un mod cu mai multe fire (de exemplu, executând unele sarcini legate de I/O). În astfel de cazuri, puteți utiliza clasa Thread pentru a crea un fir nou și pentru a rula unele procesări pe un fir separat.

De exemplu:

$task = noua clasă extinde Thread ( private $response; public function run() ( $content = file_get_contents("http://google.com"); preg_match("~ (.+)~", $conținut, $potriviri); $this->response = $match; ) ); $task->start() && $task->join(); var_dump($task->response); // șir (6) „Google”

Aici metoda de rulare este procesarea noastră, care va fi executată într-un fir nou. Când Thread::start este apelat, este generat un nou thread și este apelată metoda de rulare. Apoi unim firul copil înapoi la firul principal apelând Thread::join , care se va bloca până când firul copil se va termina de execuție. Acest lucru asigură că sarcina se termină de executat înainte de a încerca să tipărim rezultatul (care este stocat în $task->response).

Este posibil să nu fie de dorit să poluăm o clasă cu responsabilități suplimentare asociate cu logica fluxului (inclusiv responsabilitatea definirii unei metode de rulare). Putem distinge astfel de clase prin moștenirea lor din clasa Threaded. Apoi pot fi rulate într-un alt thread:

Class Task extinde Threaded ( public $response; funcția publică someWork() ( $content = file_get_contents("http://google.com"); preg_match("~ (.+) ~", $content, $match); $ this->response = $potrivește; ) ) $sarcină = sarcină nouă; $thread = clasă nouă($sarcină) extinde Thread (privată $sarcină; funcția publică __construct(Thread $sarcină) ( $aceasta->sarcină = $sarcină; ) funcția publică run() ( $aceasta->sarcină->someWork( ); )); $thread->start() && $thread->join(); var_dump($sarcină->răspuns);

Orice clasă care trebuie rulată într-un fir separat trebuie sa moștenește din clasa Threaded. Acest lucru se datorează faptului că oferă capabilitățile necesare pentru a efectua procesări pe diferite fire, precum și securitate implicită și interfețe utile (cum ar fi sincronizarea resurselor).

Să aruncăm o privire la ierarhia claselor oferită de extensia pthreads:

Filet (implementează Traversable, Collectable) Thread Worker Pool Volatile

Am acoperit și am învățat deja elementele de bază ale claselor Thread și Threaded, acum să aruncăm o privire la celelalte trei (Worker, Volatile și Pool).

Reutilizarea firelor

Începerea unui fir nou pentru fiecare sarcină care trebuie paralelizată este destul de costisitoare. Acest lucru se datorează faptului că o arhitectură comun-nimic trebuie implementată în pthreads pentru a realiza multithreading în PHP. Ceea ce înseamnă că întregul context de execuție al instanței curente a interpretului PHP (inclusiv fiecare clasă, interfață, trăsătură și funcție) trebuie copiat pentru fiecare fir creat. Deoarece acest lucru are un impact vizibil asupra performanței, fluxul ar trebui să fie întotdeauna reutilizat ori de câte ori este posibil. Threadurile pot fi reutilizate în două moduri: folosind Workers sau folosind Pools.

Clasa Worker este folosită pentru a efectua o serie de sarcini sincron în cadrul unui alt fir. Acest lucru se face prin crearea unei noi instanțe Worker (care creează un fir nou), apoi împingând sarcini în stiva acelui fir separat (folosind Worker::stack).

Iată un mic exemplu:

Class Task se extinde Threaded ( private $valoare; funcția publică __construct(int $i) ( $this->value = $i; ) public function run() ( usleep(250000); echo "Task: ($this->value) \n"; ) ) $lucrător = nou Lucrător(); $lucrător->start(); for ($i = 0; $i stack(new Task($i)); ) while ($worker->collect()); $worker->shutdown();

În exemplul de mai sus, 15 sarcini pentru un nou obiect $worker sunt împinse în stivă prin metoda Worker::stack și apoi sunt procesate în ordinea în care au fost împinse. Metoda Worker::collect, așa cum se arată mai sus, este utilizată pentru a curăța sarcinile de îndată ce se termină de executat. Cu ea, într-o buclă while, blocăm firul principal până când toate sarcinile de pe stivă sunt finalizate și șterse - înainte de a apela Worker::shutdown . Terminarea timpurie a unui lucrător (adică în timp ce încă mai sunt sarcini care trebuie finalizate) va bloca în continuare firul principal până când toate sarcinile își vor finaliza execuția, doar că sarcinile nu vor fi colectate de gunoi (ceea ce implică pierderi de memorie).

Clasa Worker oferă câteva alte metode legate de stiva sa de sarcini, inclusiv Worker::unstack pentru eliminarea ultimei sarcini stivuite și Worker::getStacked pentru obținerea numărului de sarcini din stiva de execuție. Stiva unui lucrător conține doar sarcinile care trebuie executate. Odată ce o sarcină din stivă a fost finalizată, aceasta este eliminată și plasată pe o stivă separată (internă) pentru colectarea gunoiului (folosind metoda Worker::collect).

O altă modalitate de a reutiliza un fir de execuție în mai multe sarcini este utilizarea unui pool de fire de execuție (prin clasa Pool). Un grup de fire de execuție folosește un grup de lucrători pentru a permite executarea sarcinilor simultan, în care factorul de concurență (numărul de fire de execuție cu care operează) este setat la crearea grupului.

Să adaptăm exemplul de mai sus pentru a folosi un grup de lucrători:

Class Task se extinde Threaded ( private $valoare; funcția publică __construct(int $i) ( $this->value = $i; ) public function run() ( usleep(250000); echo "Task: ($this->value) \n"; ) ) $pool = pool nou(4); for ($i = 0; $i submit(new Task($i)); ) while ($pool->collect()); $pool->shutdown();

Există câteva diferențe notabile atunci când utilizați o piscină, spre deosebire de un lucrător. În primul rând, pool-ul nu trebuie să fie pornit manual; acesta începe să execute sarcini de îndată ce acestea devin disponibile. În al doilea rând, noi trimite sarcini la piscină, nu pune-le pe un teanc. În plus, clasa Pool nu moștenește din Threaded și, prin urmare, nu poate fi transmisă altor fire (spre deosebire de Worker).

Cum bun antrenament Pentru lucrători și piscine, ar trebui să le curățați întotdeauna sarcinile de îndată ce acestea sunt finalizate și apoi să le terminați manual. Firele create folosind clasa Thread trebuie, de asemenea, atașate firului părinte.

pthreads și (im)mutabilitatea

Ultima clasă pe care o vom atinge este Volatile, o nouă adăugare la pthreads v3. Imuabilitatea a devenit un concept important în pthreads, deoarece fără ea, performanța suferă semnificativ. Prin urmare, în mod implicit, proprietățile claselor Threaded care sunt ele însele obiecte Threaded sunt acum imuabile și, prin urmare, nu pot fi suprascrise după atribuirea lor inițială. Mutabilitatea explicită pentru astfel de proprietăți este în prezent preferată și poate fi încă obținută folosind noua clasă Volatile.

Să ne uităm la un exemplu care va demonstra noile restricții de imuabilitate:

Class Task extinde Threaded // o clasă Threaded (funcția publică __construct() ( $this->data = new Threaded(); // $this->data nu poate fi suprascris, deoarece este o proprietate Threaded a unei clase Threaded) ) $task = new class(new Task()) extinde Thread ( // o clasă Threaded, deoarece Thread extinde Threaded funcția publică __construct($tm) ( $this->threadedMember = $tm; var_dump($this->threadedMember-> date); // obiect(Threaded)#3 (0) () $this->threadedMember = new StdClass(); // invalid, deoarece proprietatea este un membru Threaded al unei clase Threaded ));

Proprietățile filetate ale claselor volatile, pe de altă parte, sunt mutabile:

Class Task extinde Volatile (funcția publică __construct() ( $this->data = new Threaded(); $this->data = new StdClass(); // valid, deoarece suntem într-o clasă volatilă ) ) $task = new class(new Task()) extinde Thread (funcția publică __construct($vm) ( $this->volatileMember = $vm; var_dump($this->volatileMember->data); // obiect(stdClass)#4 (0) () // încă invalid, deoarece Volatile extinde Threaded, deci proprietatea este încă un membru Threaded al clasei Threaded $this->volatileMember = new StdClass(); ) );

Putem vedea că clasa Volatile suprascrie imuabilitatea impusă de clasa părinte Threaded pentru a oferi posibilitatea de a schimba proprietățile Threaded (precum și unset()).

Există un alt subiect de discuție pentru a acoperi tema variabilității și a clasei Volatile - matrice. În pthreads, matricele sunt turnate automat în obiecte Volatile atunci când sunt atribuite unei proprietăți a clasei Threaded. Acest lucru se datorează faptului că pur și simplu nu este sigur să manipulați o serie de mai multe contexte PHP.

Să ne uităm din nou la un exemplu pentru a înțelege mai bine unele lucruri:

$matrice = ; $sarcină = clasă nouă ($array) extinde Thread ( private $date; public function __construct(array $array) ( $this->data = $array; ) public function run() ( $this->data = 4; $ this->data = 5; print_r($this->data); ) ); $sarcină->start() && $sarcină->join(); /* Ieșire: obiect volatil ( => 1 => 2 => 3 => 4 => 5) */

Vedem că obiectele volatile pot fi tratate ca și cum ar fi matrice, deoarece acceptă operații cu matrice, cum ar fi (așa cum se arată mai sus) operatorul subset(). Cu toate acestea, clasele Volatile nu acceptă funcții de bază ale matricei, cum ar fi array_pop și array_shift. În schimb, clasa Threaded ne oferă astfel de operații ca metode încorporate.

Ca demonstrație:

$date = clasă nouă se extinde Volatil ( public $a = 1; public $b = 2; public $c = 3; ); var_dump($date); var_dump($date->pop()); var_dump($date->shift()); var_dump($date); /* Ieșire: obiect(clasă@anonim)#1 (3) ( ["a"]=> int(1) ["b"]=> int(2) ["c"]=> int(3) ) int(3) int(1) obiect(clasa@anonim)#1 (1) ( ["b"] => int(2) ) */

Alte operațiuni acceptate includ Threaded::chunk și Threaded::merge .

Sincronizare

În ultima secțiune a acestui articol, ne vom uita la sincronizarea în pthreads. Sincronizarea este o metodă care vă permite să controlați accesul la resursele partajate.

De exemplu, să implementăm un contor simplu:

$counter = noua clasa extinde Thread ( public $i = 0; public function run() ( for ($i = 0; $i i; ) ) ); $contor->start(); pentru ($i = 0; $i i; ) $counter->join(); var_dump($contor->i); // va tipări un număr de la 10 la 20

Fără utilizarea sincronizării, ieșirea nu este deterministă. Mai multe fire de execuție scriu pe aceeași variabilă fără acces controlat, ceea ce înseamnă că actualizările se vor pierde.

Să reparăm acest lucru, astfel încât să obținem rezultatul corect de 20 prin adăugarea de sincronizare:

$counter = noua clasa extinde Thread ( public $i = 0; public function run() ( $this->synchronized(function () ( for ($i = 0; $i i; ) )); ) ); $contor->start(); $contor->sincronizat(funcție ($contor) ( pentru ($i = 0; $i i; ) ), $contor); $counter->join(); var_dump($contor->i); // int(20)

Blocurile de cod sincronizate pot comunica, de asemenea, între ele folosind metodele Threaded::wait și Threaded::notify (sau Threaded::notifyAll).

Iată un increment alternativ în două bucle while sincronizate:

$counter = noua clasa extinde Thread ( public $cond = 1; public function run() ( $this->synchronized(function () (pentru ($i = 0; $i notify();); if ($this->cond) === 1) ( $this->cond = 2; $this->wait(); ) ) )); ) ); $contor->start(); $contor->sincronizat(funcție ($contor) ( if ($contor->cond !== 2) ( $contor->wait(); // așteptați ca celălalt să pornească primul ) pentru ($i = 10; $i notify(); if ($counter->cond === 2) ( $counter->cond = 1; $counter->wait(); ) ) ), $contor); $counter->join(); /* Ieșire: 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) */

Este posibil să observați condiții suplimentare care au fost plasate în jurul apelului către Threaded::wait . Aceste condiții sunt critice deoarece permit reluarea apelului sincronizat atunci când a primit o notificare și condiția specificată este adevărată. Acest lucru este important deoarece notificările pot veni din alte locuri decât atunci când Threaded::notify este apelat. Astfel, dacă apelurile la metoda Threaded::wait nu au fost incluse în condiții, vom executa apeluri de trezire false, ceea ce va duce la un comportament imprevizibil al codului.

Concluzie

Am analizat cele cinci clase ale pachetului pthreads (Threaded, Thread, Worker, Volatile și Pool) și cum este utilizată fiecare clasă. De asemenea, am aruncat o privire asupra noului concept de imuabilitate în pthreads și am oferit o scurtă prezentare generală a capabilităților de sincronizare acceptate. Cu aceste elemente de bază, acum putem începe să ne uităm la modul în care pthread-urile pot fi utilizate în cazurile din lumea reală! Acesta va fi subiectul următoarei noastre postări.

Dacă ești interesat de traducerea următoarei postări, anunță-mă: comentează pe rețelele de socializare. rețele, votează pozitiv și distribuie postarea colegilor și prietenilor.

  • programare,
  • Programare în paralel
  • Am încercat recent pthreads și am fost plăcut surprins - este o extensie care adaugă capacitatea de a lucra cu mai multe fire reale în PHP. Fără emulare, fără magie, fără falsuri - totul este real.



    Mă gândesc la o astfel de sarcină. Există o serie de sarcini care trebuie finalizate rapid. PHP are alte instrumente pentru rezolvarea acestei probleme, ele nu sunt menționate aici, articolul este despre pthreads.



    Ce sunt pthread-urile

    Asta e tot! Ei bine, aproape totul. De fapt, există ceva care poate supăra un cititor curios. Nimic din acestea nu funcționează pe PHP standard compilat cu opțiuni implicite. Pentru a vă bucura de multithreading, trebuie să aveți ZTS (Zend Thread Safety) activat în PHP.

    Configurare PHP

    Apoi, PHP cu ZTS. Nu vă deranjează marea diferență de timp de execuție față de PHP fără ZTS (37.65 vs 265.05 secunde), nu am încercat să generalizez setarea PHP. În cazul fără ZTS, am XDebug activat, de exemplu.


    După cum puteți vedea, atunci când utilizați 2 fire, viteza de execuție a programului este de aproximativ 1,5 ori mai mare decât în ​​cazul codului liniar. Când utilizați 4 fire - de 3 ori.


    Puteți observa că, deși procesorul este cu 8 nuclee, timpul de execuție al programului a rămas aproape neschimbat dacă s-au folosit mai mult de 4 fire. Se pare că acest lucru se datorează faptului că procesorul meu are 4 nuclee fizice.Pentru claritate, am descris placa sub forma unei diagrame.


    rezumat

    În PHP, este posibil să lucrați destul de elegant cu multithreading folosind extensia pthreads. Acest lucru oferă o creștere vizibilă a productivității.

    Etichete:

    • php
    • pthreads
    Adaugă etichete

    Am încercat recent pthreads și am fost plăcut surprins - este o extensie care adaugă capacitatea de a lucra cu mai multe fire reale în PHP. Fără emulare, fără magie, fără falsuri - totul este real.



    Mă gândesc la o astfel de sarcină. Există o serie de sarcini care trebuie finalizate rapid. PHP are alte instrumente pentru rezolvarea acestei probleme, ele nu sunt menționate aici, articolul este despre pthreads.



    Ce sunt pthread-urile

    Asta e tot! Ei bine, aproape totul. De fapt, există ceva care poate supăra un cititor curios. Nimic din acestea nu funcționează pe PHP standard compilat cu opțiuni implicite. Pentru a vă bucura de multithreading, trebuie să aveți ZTS (Zend Thread Safety) activat în PHP.

    Configurare PHP

    Apoi, PHP cu ZTS. Nu vă deranjează marea diferență de timp de execuție față de PHP fără ZTS (37.65 vs 265.05 secunde), nu am încercat să generalizez setarea PHP. În cazul fără ZTS, am XDebug activat, de exemplu.


    După cum puteți vedea, atunci când utilizați 2 fire, viteza de execuție a programului este de aproximativ 1,5 ori mai mare decât în ​​cazul codului liniar. Când utilizați 4 fire - de 3 ori.


    Puteți observa că, deși procesorul este cu 8 nuclee, timpul de execuție al programului a rămas aproape neschimbat dacă s-au folosit mai mult de 4 fire. Se pare că acest lucru se datorează faptului că procesorul meu are 4 nuclee fizice.Pentru claritate, am descris placa sub forma unei diagrame.


    rezumat

    În PHP, este posibil să lucrați destul de elegant cu multithreading folosind extensia pthreads. Acest lucru oferă o creștere vizibilă a productivității.



    
    Top