Flertrådad programmering i PHP med Pthreads Translation. Flertrådig datoranvändning i PHP: pthreads Path of the Jedi - med PCNTL-tillägget

Jag testade nyligen pthreads och blev positivt överraskad - det är en förlängning som lägger till möjligheten att arbeta med flera riktiga trådar i PHP. Ingen emulering, ingen magi, inga förfalskningar - allt är på riktigt.



Jag funderar på en sådan uppgift. Det finns en pool av uppgifter som måste slutföras snabbt. PHP har andra verktyg för att lösa detta problem, de nämns inte här, artikeln handlar om pthreads.



Vad är pthreads

Det är allt! Tja, nästan allt. Det finns faktiskt något som kan göra en nyfiken läsare upprörd. Inget av detta fungerar på standard PHP kompilerad med standardalternativ. För att njuta av multithreading måste du ha ZTS (Zend Thread Safety) aktiverat i din PHP.

PHP-inställning

Därefter PHP med ZTS. Var inte uppmärksam på en så stor skillnad i exekveringstid jämfört med PHP utan ZTS (37,65 vs 265,05 sekunder), jag försökte inte minska det till en gemensam nämnare PHP-inställningar. I fallet utan ZTS har jag till exempel XDebug aktiverat.


Som du kan se, när du använder 2 trådar, är hastigheten för programexekveringen ungefär 1,5 gånger högre än i fallet med linjär kod. Vid användning av 4 trådar - 3 gånger.


Du kan notera att även om processorn är 8-kärnig, så förblev programmets körtid nästan oförändrad om mer än 4 trådar användes. Det verkar som att detta beror på att min processor har 4 fysiska kärnor. För tydlighetens skull har jag avbildat plattan i form av ett diagram.


Sammanfattning

I PHP är det möjligt att arbeta ganska elegant med multithreading med hjälp av tillägget pthreads. Detta ger en märkbar ökning av produktiviteten.

Taggar: Lägg till taggar

Ibland blir det nödvändigt att utföra flera åtgärder samtidigt, till exempel att kontrollera ändringar i en databastabell och göra ändringar i en annan. Dessutom, om en av operationerna (till exempel att kontrollera ändringar) tar mycket tid, är det uppenbart att sekventiell exekvering inte kommer att säkerställa resursbalansering.

För att lösa denna typ av problem använder programmering multithreading - varje operation placeras i en separat tråd med en tilldelad mängd resurser och fungerar inom den. Med detta tillvägagångssätt kommer alla uppgifter att slutföras separat och oberoende.

Även om PHP inte stöder multithreading, finns det flera metoder för att emulera det, om vilka vi ska prata Nedan.

1. Köra flera kopior av skriptet - en kopia per operation

//woman.php if (!isset($_GET["tråd"])) (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" ) (hitta_annan_en( ); )

När vi kör det här skriptet utan parametrar kör det automatiskt två kopior av sig självt, med operations-ID:n ("tråd=make_me_happy" och "thread=make_me_rich"), som initierar exekveringen av de nödvändiga funktionerna.

På så sätt uppnår vi det önskade resultatet - två operationer utförs samtidigt - men detta är naturligtvis inte multithreading, utan bara en krycka för att utföra uppgifter samtidigt.

2. Path of the Jedi - med PCNTL-tillägget

PCNTL är ett tillägg som låter dig arbeta fullt ut med processer. Förutom hantering stöder den att skicka meddelanden, kontrollera status och ställa in prioriteringar. Så här ser det tidigare skriptet med PCNTL ut:

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

Det ser ganska förvirrande ut, låt oss gå igenom det rad för rad.

På den första raden "kaffar" vi den aktuella processen (gaffel kopierar en process samtidigt som värdena för alla variabler bevaras), delar upp den i två processer (aktuell och underordnad) som löper parallellt.

För att förstå var vi är det här ögonblicket, i en barn- eller moderprocess returnerar pcntl_fork-funktionen 0 för barnet och process-ID för modern. Därför, på den andra raden, tittar vi på $pid, om den är noll, är vi i barnprocessen - vi kör funktionen, annars är vi i mamman (rad 4), då skapar vi en annan process och utföra uppgiften på samma sätt.

Skriptkörningsprocess:

Således skapar skriptet ytterligare två underordnade processer, som är dess kopior och innehåller samma variabler med liknande värden. Och med hjälp av identifieraren som returneras av funktionen pcntl_fork, tar vi reda på vilken tråd vi för närvarande befinner oss i och utför nödvändiga åtgärder.

Det verkar, PHP-utvecklare parallellism används sällan. Jag kommer inte att prata om enkelheten med synkron kod; enkeltrådad programmering är naturligtvis enklare och tydligare, men ibland kan lite användning av parallellitet ge en märkbar ökning av prestanda.

I den här artikeln ska vi ta en titt på hur multithreading kan uppnås i PHP med tillägget pthreads. För att göra detta behöver du ZTS-versionen (Zend Thread Safety) av PHP 7.x installerad, tillsammans med installerat tillägg pthreads v3. (I skrivande stund, i PHP 7.1, kommer användare att behöva installera från huvudgrenen i pthreads-förvaret - se tillägg från tredje part.)

Ett litet förtydligande: pthreads v2 är avsedd för PHP 5.x och stöds inte längre, pthreads v3 är för PHP 7.x och utvecklas aktivt.

Efter en sådan utvikning, låt oss gå direkt till saken!

Bearbetning av engångsuppgifter

Ibland vill du bearbeta engångsuppgifter på ett flertrådigt sätt (till exempel exekvera någon I/O-bunden uppgift). I sådana fall kan du använda klassen Thread för att skapa en ny tråd och köra en del bearbetning på en separat tråd.

Till exempel:

$task = ny klass utökar tråden ( privat $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); // sträng (6) "Google"

Här är körmetoden vår bearbetning, som kommer att köras i en ny tråd. När Thread::start anropas skapas en ny tråd och körmetoden anropas. Vi sammanfogar sedan den underordnade tråden tillbaka till huvudtråden genom att anropa Thread::join , som kommer att blockera tills den underordnade tråden har körts färdigt. Detta säkerställer att uppgiften avslutas innan vi försöker skriva ut resultatet (som lagras i $task->response).

Det kanske inte är önskvärt att förorena en klass med ytterligare ansvar förknippade med flödeslogik (inklusive ansvaret för att definiera en körmetod). Vi kan särskilja sådana klasser genom att ärva dem från den trådade klassen. Sedan kan de köras i en annan tråd:

Klassuppgift utökar Threaded ( public $response; public function someWork() ( $content = file_get_contents("http://google.com"); preg_match("~ (.+) ~", $content, $matches); $ this->response = $matches; ) ) $task = ny uppgift; $thread = new class($task) utökar tråden ( privat $task; public function __construct(Threaded $task) ( $this->task = $task; ) public function run() ( $this->task->someWork( ); ) ); $thread->start() && $thread->join(); var_dump($task->svar);

Alla klasser som behöver köras i en separat tråd måsteärva från klassen Threaded. Detta beror på att det ger nödvändiga möjligheter för att utföra bearbetning på olika trådar, såväl som implicit säkerhet och användbara gränssnitt (som resurssynkronisering).

Låt oss ta en titt på klasshierarkin som erbjuds av tillägget pthreads:

Gängad (implementerar Traversable, Collectable) Thread Worker Volatile Pool

Vi har redan täckt och lärt oss grunderna i trådarna och trådade klasserna, låt oss nu ta en titt på de andra tre (Worker, Volatile och Pool).

Återanvändning av trådar

Att starta en ny tråd för varje uppgift som behöver parallelliseras är ganska dyrt. Detta beror på att en gemensam-ingenting-arkitektur måste implementeras i pthreads för att uppnå multithreading inom PHP. Vilket innebär att hela exekveringskontexten för den aktuella instansen av PHP-tolken (inklusive varje klass, gränssnitt, egenskap och funktion) måste kopieras för varje skapad tråd. Eftersom detta har en märkbar prestandapåverkan bör strömmen alltid återanvändas när det är möjligt. Trådar kan återanvändas på två sätt: med Workers eller Pools.

Worker-klassen används för att utföra ett antal uppgifter synkront inom en annan tråd. Detta görs genom att skapa en ny Worker-instans (som skapar en ny tråd) och sedan skjuta uppgifter till den separata trådens stack (med Worker::stack).

Här är ett litet exempel:

Klassuppgift utökar Threaded ( privat $värde; offentlig funktion __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(ny uppgift($i)); ) while ($worker->collect()); $worker->shutdown();

I exemplet ovan skjuts 15 uppgifter för ett nytt $worker-objekt till stacken via Worker::stack-metoden, och sedan bearbetas de i den ordning de skickades. Worker::collect-metoden, som visas ovan, används för att städa upp uppgifter så fort de är klara. Med den, inuti en while-loop, blockerar vi huvudtråden tills alla uppgifter i stacken är klara och rensade - innan vi anropar Worker::shutdown . Att avsluta en arbetare tidigt (d.v.s. medan det fortfarande finns uppgifter att slutföra) kommer fortfarande att blockera huvudtråden tills alla uppgifter har slutfört sin exekvering, bara att uppgifterna inte kommer att samlas in för skräp (vilket innebär att representera minnesläckor).

Worker-klassen tillhandahåller flera andra metoder relaterade till dess uppgiftsstack, inklusive Worker::unstack för att ta bort den senast staplade uppgiften och Worker::getStacked för att få antalet uppgifter i exekveringsstacken. En arbetarstack innehåller endast de uppgifter som behöver utföras. När en uppgift i högen har slutförts tas den bort och placeras på en separat (intern) hög för sophämtning (med Worker::collect-metoden).

Ett annat sätt att återanvända en tråd över flera uppgifter är att använda en trådpool (via klassen Pool). En trådpool använder en grupp arbetare för att göra det möjligt att utföra uppgifter samtidigt, där samtidighetsfaktorn (antalet pooltrådar den arbetar med) ställs in när poolen skapas.

Låt oss anpassa exemplet ovan för att använda en pool av arbetare:

Klassuppgift utökar Threaded ( privat $värde; offentlig funktion __construct(int $i) ( $this->value = $i; ) public function run() (usleep(250000); echo "Task: ($this->value) \n"; ) ) $pool = ny Pool(4); for ($i = 0; $i submit(new Task($i)); ) while ($pool->collect()); $pool->shutdown();

Det finns några anmärkningsvärda skillnader när du använder en pool i motsats till en arbetare. För det första behöver poolen inte startas manuellt, den börjar utföra uppgifter så snart de blir tillgängliga. För det andra, vi skicka uppgifter till poolen, inte lägg dem på en hög. Dessutom ärver inte klassen Pool från Threaded och kan därför inte skickas till andra trådar (till skillnad från Worker).

Hur bra övning För arbetare och pooler bör du alltid rensa upp deras uppgifter så snart de är klara och sedan manuellt avsluta dem själva. Trådar skapade med klassen Thread måste också kopplas till den överordnade tråden.

pthreads och (o)föränderlighet

Den sista klassen vi kommer att beröra är Volatile, ett nytt tillägg till pthreads v3. Oföränderlighet har blivit ett viktigt begrepp i pthreads för utan det blir prestanda avsevärt lidande. Därför, som standard, är egenskaperna för trådade klasser som själva är trådade objekt nu oföränderliga, och därför kan de inte skrivas över efter deras första tilldelning. Explicit mutabilitet för sådana egenskaper är för närvarande att föredra och kan fortfarande uppnås med den nya Volatile-klassen.

Låt oss titta på ett exempel som visar de nya oföränderlighetsbegränsningarna:

Klassuppgift utökar Threaded // en Threaded-klass ( offentlig funktion __construct() ( $this->data = new Threaded(); // $this->data kan inte skrivas över, eftersom det är en Threaded-egenskap för en Threaded-klass ) ) $task = new class(new Task()) utökar tråden ( // en trådad klass, eftersom tråden utökar trådad offentlig funktion __construct($tm) ( $this->threadedMember = $tm; var_dump($this->threadedMember-> data); // object(Threaded)#3 (0) () $this->threadedMember = new StdClass(); // ogiltig, eftersom egenskapen är en trådad medlem av en trådad klass ) );

Gängade egenskaper hos Volatile-klasser är å andra sidan föränderliga:

Class Task utökar Volatile ( offentlig funktion __construct() ( $this->data = new Threaded(); $this->data = new StdClass(); // giltigt, eftersom vi är i en volatil klass ) ) $task = new class(new Task()) utökar tråden ( offentlig funktion __construct($vm) ( $this->volatileMember = $vm; var_dump($this->volatileMember->data); // object(stdClass)#4 (0) () // fortfarande ogiltig, eftersom Volatile utökar Threaded, så egenskapen är fortfarande en Threaded-medlem i en Threaded-klass $this->volatileMember = new StdClass(); ) );

Vi kan se att klassen Volatile åsidosätter oföränderligheten som påtvingas av den överordnade Threaded-klassen för att ge möjligheten att ändra Threaded-egenskaper (liksom unset()).

Det finns ett annat diskussionsämne för att täcka ämnet variabilitet och klassen Volatile - arrayer. I pthreads casts arrayer automatiskt till Volatile objekt när de tilldelas en egenskap i klassen Threaded. Detta beror på att det helt enkelt inte är säkert att manipulera en uppsättning av flera PHP-kontexter.

Låt oss titta på ett exempel igen för att förstå några saker bättre:

$array = ; $task = new class($array) utökar tråden (privat $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(); /* Output: Volatile Object ( => 1 => 2 => 3 => 4 => 5) */

Vi ser att flyktiga objekt kan behandlas som om de vore arrayer eftersom de stöder arrayoperationer som (som visas ovan) subset() operatorn. Volatile-klasser stöder dock inte grundläggande array-funktioner som array_pop och array_shift. Istället ger klassen Threaded oss ​​sådana operationer som inbyggda metoder.

Som en demonstration:

$data = ny klass utökar Volatile ( public $a = 1; public $b = 2; public $c = 3; ); var_dump($data); var_dump($data->pop()); var_dump($data->shift()); var_dump($data); /* Utdata: objekt(klass@anonym)#1 (3) ( ["a"]=> int(1) ["b"]=> int(2) ["c"]=> int(3) ) int(3) int(1) objekt(klass@anonym)#1 (1) ( ["b"]=> int(2) ) */

Andra funktioner som stöds inkluderar Threaded::chunk och Threaded::merge .

Synkronisering

I det sista avsnittet av denna artikel kommer vi att titta på synkronisering i pthreads. Synkronisering är en metod som låter dig kontrollera åtkomsten till delade resurser.

Låt oss till exempel implementera en enkel räknare:

$counter = ny klass utökar tråden ( public $i = 0; public function run() (för ($i = 0; $i i; ) ) ); $counter->start(); för ($i = 0; $i i; ) $counter->join(); var_dump($counter->i); // kommer att skriva ut ett nummer från 10 till 20

Utan användning av synkronisering är utsignalen inte deterministisk. Flera trådar skriver till samma variabel utan kontrollerad åtkomst, vilket innebär att uppdateringar kommer att gå förlorade.

Låt oss fixa detta så att vi får rätt utdata på 20 genom att lägga till timing:

$counter = ny klass utökar Tråd ( public $i = 0; public function run() ( $this->synchronized(function () (för ($i = 0; $i i; ))); ) ); $counter->start(); $counter->synchronized(funktion ($counter) (för ($i = 0; $i i; ) ), $counter); $counter->join(); var_dump($counter->i); // int(20)

Synkroniserade kodblock kan också kommunicera med varandra med metoderna Threaded::wait och Threaded::notify (eller Threaded::notifyAll).

Här är ett alternativt steg i två synkroniserade while-loopar:

$counter = ny klass utökar tråden ( public $cond = 1; public function run() ( $this->synchronized(function () (för ($i = 0; $i notify(); if ($this->cond) === 1) ( $this->cond = 2; $this->wait(); ) ) )); ) ); $counter->start(); $counter->synchronized(function ($counter) ( if ($counter->cond !== 2) ( $counter->wait(); // vänta tills den andra startar först ) för ($i = 10; $i notify(); if ($counter->cond === 2) ( $counter->cond = 1; $counter->wait(); ) ) ), $counter); $counter->join(); /* Utdata: 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) */

Du kan märka ytterligare villkor som har placerats kring samtalet till Threaded::wait . Dessa villkor är kritiska eftersom de tillåter den synkroniserade återuppringningen att återupptas när den har fått ett meddelande och det angivna villkoret är sant. Detta är viktigt eftersom aviseringar kan komma från andra ställen än när Threaded::notify anropas. Således, om anrop till Threaded::wait-metoden inte var inneslutna i villkor, kommer vi att köra falska väckningssamtal, vilket kommer att leda till oförutsägbart kodbeteende.

Slutsats

Vi tittade på de fem klasserna i pthreads-paketet (Threaded, Thread, Worker, Volatile och Pool) och hur varje klass används. Vi tog också en titt på det nya konceptet med oföränderlighet i pthreads och gav en kort översikt över de synkroniseringsmöjligheter som stöds. Med dessa grunder på plats kan vi nu börja titta på hur pthreads kan användas i verkliga fall! Detta kommer att bli ämnet för vårt nästa inlägg.

Om du är intresserad av översättningen av nästa inlägg, låt mig veta: kommentera på sociala medier. nätverk, rösta upp och dela inlägget med kollegor och vänner.

  • Programmering,
  • Parallell programmering
  • Jag testade nyligen pthreads och blev positivt överraskad - det är en förlängning som lägger till möjligheten att arbeta med flera riktiga trådar i PHP. Ingen emulering, ingen magi, inga förfalskningar - allt är på riktigt.



    Jag funderar på en sådan uppgift. Det finns en pool av uppgifter som måste slutföras snabbt. PHP har andra verktyg för att lösa detta problem, de nämns inte här, artikeln handlar om pthreads.



    Vad är pthreads

    Det är allt! Tja, nästan allt. Det finns faktiskt något som kan göra en nyfiken läsare upprörd. Inget av detta fungerar på standard PHP kompilerad med standardalternativ. För att njuta av multithreading måste du ha ZTS (Zend Thread Safety) aktiverat i din PHP.

    PHP-inställning

    Därefter PHP med ZTS. Har inget emot den stora skillnaden i exekveringstid jämfört med PHP utan ZTS (37,65 vs 265,05 sekunder), jag försökte inte generalisera PHP-inställningen. I fallet utan ZTS har jag till exempel XDebug aktiverat.


    Som du kan se, när du använder 2 trådar, är hastigheten för programexekveringen ungefär 1,5 gånger högre än i fallet med linjär kod. Vid användning av 4 trådar - 3 gånger.


    Du kan notera att även om processorn är 8-kärnig, så förblev programmets körtid nästan oförändrad om mer än 4 trådar användes. Det verkar som att detta beror på att min processor har 4 fysiska kärnor. För tydlighetens skull har jag avbildat plattan i form av ett diagram.


    Sammanfattning

    I PHP är det möjligt att arbeta ganska elegant med multithreading med hjälp av tillägget pthreads. Detta ger en märkbar ökning av produktiviteten.

    Taggar:

    • php
    • pthreads
    Lägg till taggar

    Jag testade nyligen pthreads och blev positivt överraskad - det är en förlängning som lägger till möjligheten att arbeta med flera riktiga trådar i PHP. Ingen emulering, ingen magi, inga förfalskningar - allt är på riktigt.



    Jag funderar på en sådan uppgift. Det finns en pool av uppgifter som måste slutföras snabbt. PHP har andra verktyg för att lösa detta problem, de nämns inte här, artikeln handlar om pthreads.



    Vad är pthreads

    Det är allt! Tja, nästan allt. Det finns faktiskt något som kan göra en nyfiken läsare upprörd. Inget av detta fungerar på standard PHP kompilerad med standardalternativ. För att njuta av multithreading måste du ha ZTS (Zend Thread Safety) aktiverat i din PHP.

    PHP-inställning

    Därefter PHP med ZTS. Har inget emot den stora skillnaden i exekveringstid jämfört med PHP utan ZTS (37,65 vs 265,05 sekunder), jag försökte inte generalisera PHP-inställningen. I fallet utan ZTS har jag till exempel XDebug aktiverat.


    Som du kan se, när du använder 2 trådar, är hastigheten för programexekveringen ungefär 1,5 gånger högre än i fallet med linjär kod. Vid användning av 4 trådar - 3 gånger.


    Du kan notera att även om processorn är 8-kärnig, så förblev programmets körtid nästan oförändrad om mer än 4 trådar användes. Det verkar som att detta beror på att min processor har 4 fysiska kärnor. För tydlighetens skull har jag avbildat plattan i form av ett diagram.


    Sammanfattning

    I PHP är det möjligt att arbeta ganska elegant med multithreading med hjälp av tillägget pthreads. Detta ger en märkbar ökning av produktiviteten.



    
    Topp