login  Naam:   Wachtwoord: 
Registreer je!
 Tutorials

Tutorials > PHP


Gegevens:
Geschreven door:
Rik
Moeilijkheidsgraad:
Moeilijk
Hits:
10570
Punten:
Aantal punten:
 
Aantal stemmen:
0
Stem:
Niet ingelogd
Nota's:
 Lees de nota's (3)
 

Tutorial:

Multithreading strategie



1. Inleiding

Php is een taal waarin bijna alles mogelijk is door middel van talloze classes, frameworks en extensies. Ook zware en langdurige taken zoals het renderen van afbeeldingen of het uploaden naar externe servers behoort tot de mogelijkheden. Het grootste probleem daarbij is dat zulke taken vaak herhaaldelijk moeten worden uitgevoerd en niet al te efficiënt zijn. Het kost dan buitensporig veel tijd omdat php steeds moet wachten tot een bepaalde langzame bewerking is voltooid. Een grote snelheidswinst kan behaald worden als enkele langdurige (niet reken intensieve) taken tegelijkertijd worden uitgevoerd: multithreading. In deze tutorial zal ik mijn zoektocht naar een goede strategie om multithreading in php te implementeren uiteenzetten.


 top
2. Bestaande mogelijkheden

Php heeft de pcntl extensie [1] (Process CoNTroL), waarmee processen kunnen worden beheerd. Dat werkt ongeveer zoals in talen die zijn ontworpen om te kunnen multithreaden, bijvoorbeeld C. Zo kunnen er processen “geforkt” worden en er kunnen signalen tussen verschillende “child” processen worden verstuurd. Hoewel de mogelijkheden daarvan zeer interessant zijn, ga ik daar geen gebruik van maken. Deze methode heeft namelijk een aantal belangrijke nadelen. Het werkt momenteel alleen op Unix servers, waardoor het voor programmeurs op Windows onhandig is. Natuurlijk draaien de meeste echte webservers Unix, maar het belangrijkste nadeel is dat deze extensie alleen gebruikt kan worden als php wordt geladen vanaf de command line. Natuurlijk is dat geen probleem als het script wordt geladen met een cronjob, maar het is erg handig als dit ook werkt via de webserver.

Deze optie valt af en ik ga op zoek naar strategieën die anderen hebben bedacht om te multithreaden. Alex Lau heeft een even simpele als innovatieve oplossing gerealiseerd voor php 4 [2]. Met de komst van php 5 kan deze code een stuk worden verbeterd. Dat is gebeurd door een persoon die zich W-Shadow [3] noemt en Cameron Laird [4]. Het idee is als volgt:

  1. Het hoofdscript (main.php) wordt via de webserver geladen.
  2. In main.php wordt een http request gemaakt naar een bestand child.php.
  3. In child.php wordt een taak uitgevoerd, waarna het eventueel een statusbericht output.
  4. Stap 2 wordt herhaald voor het gewenste aantal threads.
  5. In main.php wordt gewacht tot één van de threads klaar is met stap 3.
  6. Eventueel wordt het resultaat van de thread uit stap 4 verwerkt en stap 5 wordt nogmaals uitgevoerd.
  7. Uiteindelijk zijn alle threads klaar en kan main.php stoppen.

Het maken van http requests in stap 2 is in principe eenvoudig, maar het moet wel op zo’n manier gebeuren dat ze in stap 5 op volgorde van eindigen kunnen worden verwerkt. Dat is handig, want zo kan een thread die snel klaar is gelijk worden verwerkt, terwijl threads die meer tijd nodig hebben nog bezig zijn. In het originele ontwerp van Alex Lau gebeurde dit niet. Daar konden een aantal threads worden gestart en ze werden op dezelfde volgorde weer verwerkt. Op zich is dat ook multithreaded, want er worden verschillende taken tegelijkertijd uitgevoerd, maar het is onmogelijk om zo altijd een bepaald aantal threads tegelijk uit te laten voeren. Op deze manier wordt dus geen maximale efficiëntie behaald.

Het slimme is dat main.php niet simpelweg wordt geopend met file_get_contents(), maar met fsockopen(). Die eerste functie vraagt het bestand op en returned de inhoud ervan in één keer, dat betekent: het wacht tot de code in child.php allemaal is uitgevoerd. Dit maakt multithreaden onmogelijk. fsockopen() daarentegen opent een zogenaamde “socket stream” naar de webserver, een soort kanaal waardoor gegevens tussen main.php en de webserver kunnen worden uitgewisseld. De webserver begrijpt alleen http requests, dus in main.php zal een geldige aanvraag worden geschreven naar de stream waarin child.php wordt aangevraagd. Nu kan de stream op elke moment worden uitgelezen, dus eerst kunnen nog meer threads worden gestart in stap 4. Het is de functie stream_select() die in stap 5 bepaalt welke thread als eerste klaar is. Deze functie wordt aangeroepen met een array gevuld met alle streams. Het zal nu (een opgegeven tijd) wachten totdat er activiteit is op één van de streams. Die activiteit geeft aan dat een thread klaar is en stap 6 wordt uitgevoerd. Door stap 5 in een oneindige lus uit te voeren kan main.php wachten totdat alle threads klaar zijn, om vervolgens zelf te eindigen met stap 7.

Natuurlijk kunnen main.php en child.php in één bestand worden samengevoegd. Het is dan zaak dat er bij het starten van een thread wordt meegegeven dat het om een thread gaat en dat dat in main.php ook wordt gedetecteerd. Als die detectie faalt, zal de webserver zeer waarschijnlijk crashen omdat elke keer dat een thread wordt gestart, die weer nieuwe threads start, die weer nieuwe threads starten enzovoort. Als dit wordt voorkomen en als de code goed wordt opgezet kan op deze manier heel eenvoudig met multithreading worden gewerkt.


 top
3. Opzetten standaard objecten

Eigenlijk is het heel specifiek om een thread te openen via een http request, dus ik ben dit idee stap voor stap opnieuw gaan opbouwen. Daarbij maak ik ook gebruik van de door W-Shadow voorgestelde opsplitsing in een Thread- en een ThreadManager object, later zal de HttpThread terugkeren. De ThreadManager zal verschillende Threads starten en het wachten op een geëindigde thread afhandelen. Dit is dus een asynchroon proces. Het is handig om hiervoor standaard objecten te maken, zo worden de functies van een Thread:

  • start(), code om de thread te starten.
  • stop(), code om de thread voortijdig te stoppen.
  • wait(), code om te wachten tot de thread klaar is.
  • finish(), een soort shortcut, waarin een ongestart thread wordt gestart, er wordt gewacht tot die klaar is en het resultaat wordt verwerkt.
  • _ready(), code die wordt aangeroepen om het resultaat te verwerken.
  • getStatus(), geef een statuscode terug.

De verschillende statussen heb ik met constanten benoemd: IDLE, RUNNING, READY, FINISHED en STOPPED. Een ongestart thread is IDLE, na starten is het RUNNING, als er resultaat is, is het READY en na het verwerken van het resultaat FINISHED. Als de thread tussentijds wordt beëindigt is het STOPPED. Deze codes spreken eigenlijk voor zich, maar maken de code een stuk duidelijker. De wait() en de finish() functie zijn eigenlijk overbodig bij multithreading, want dan hoor je niet op een specifieke thread te wachten. Toch heb ik dat ingebouwd omdat het in bepaalde gevallen wel handig kan zijn.

Nu de ThreadManager:

  • addThread(), voeg thread objecten toe.
  • start(), start threads.
  • wait(), wacht tot threads klaar zijn, en handel ze af door hun finish() functie aan te roepen.
  • run(), combineer start() en wait() om een bepaald aantal threads parallel te laten lopen.
  • getStatus(), geef een statuscode terug.

Ook hier komen de status constanten IDLE en RUNNING terug, deze spreken voor zich. Merk op dat de wait() functies van een Thread en een ThreadManager exact hetzelfde doen, met als enige verschil dat bij een Thread wordt geselecteerd op enkel de eigen stream en bij de ThreadManager op alle streams van de actieve Threads. De ThreadManager kan zo worden geprogrammeerd dat die configuratie opties gebruikt waarin kan worden opgegeven:

  • hoeveel Threads er tegelijkertijd actief mogen zijn
  • hoe lang de run() functie zijn werk moet doen, en:
    • hoeveel Threads die mag starten
    • hoeveel Threads die mag beëindigen
    • hoe lang die zijn werk moet doen
  • hoe lang er per Thread moet worden gewacht tot die klaar is

Vooral die laatste optie is belangrijk, want als er geen moment gewacht wordt tot er een Thread klaar is, zal de hele procedure ook heel vaak worden uitgevoerd en dat kost onnodig veel rekenkracht. Natuurlijk is er ook ruimte voor configuratie in de Thread zelf. Met deze twee objecten is een duidelijke basis gelegd waarmee verschillende soorten Threads en ThreadManagers kunnen worden gemaakt.


 top
4. Soorten threads

De stream_select() functie werkt met alle soorten streams, waarom zouden we het dan bij sockets houden? Met proc_open() kunnen processen worden gestart, niet te verwarren met de pcntl functies. Eigenlijk is dit de veredelde variant van de exec() functie, die het mogelijk maakt om via pipes te communiceren. Als anologie: proc_open() is de uitgebreide versie van exec(), net als fsockopen() als uitgebreide versie van file_get_contents() kan worden gebruikt. Pipes zijn een concept in processen waar ik verder niet op in zal gaan, het gaat erom dat het in feite ook streams zijn, wat ze geschikt maakt voor gebruik met stream_select(). Als je een php script als proces uit wilt voeren is het het makkelijkste om de gegevens niet via de pipe door te geven, maar direct bij het starten van het proces:

php –f thread.php -- gegevens

Meer informatie over het op deze manier utivoeren van php scripts via de command line staat op php.net. [5] In thread.php kunnen die gegevens worden benaderd via $_SERVER[‘argv’]. Nu kunnen Threads dus niet allen via de webserver, maar ook direct via de command line worden gestart. Dit zorgt voor meer flexibiliteit en het mooiste: deze methode werkt zelfs op Windows. Voor dit soort Threads kan een ProcessThread worden gemaakt. Om duidelijk te maken dat de tot nu toe gebruikte ThreadManager met streams werkt is het verstandig deze naar StreamThreadManager te hernoemen.

Een heel ander soort thread kan met curl (Client URL Library) [6] worden gemaakt. Curl is eigenlijk een zeer uitgebreide wrapper voor het http protocol en werkt daardoor erg gemakkelijk. Er is geen mogelijkheid om de stream te benaderen, maar er is een oplossing om toch te multithreaden. Een erg mooi voorbeeld [7] staat bij de commentaren van de curl_multi_exec() functie. De werking is grofweg gelijk aan de benadering met streams en kan worden omgezet in CurlThread en CurlThreadManager objecten. Op de exacte werking van de mechanismes die curl gebruikt ga ik nu niet in.

Om ruimte open te laten voor nieuwe soorten Threads heb ik een alternatief gemaakt voor de wait() functie in de standaard ThreadManager. Daarin wordt simpelweg per actieve thread de wait() functie aangeroepen waarbij wordt opgegeven dat die een bepaalde tijd moet wachten. Het is dan aan de Thread zelf om daar mee om te gaan. Voor Stream- en CurlThreads zijn de eerder genoemde wachtmethoden logischer, maar zo is er alweer extra flexibiliteit. Het wordt nu ook mogelijk om binnen één ThreadManager verschillende soorten Threads uit te voeren. Natuurlijk zou je ook verschillende ThreadManagers als Thread kunnen gebruiken om hetzelfde te bereiken, maar daar wordt het script niet duidelijker van.

Ten slotte is het handig om toegepaste Threads te maken die functies in php bestanden aan kunnen roepen. In het voorbeeld waarbij main.php het bestand child.php aanroept kan main.php ook extra informatie over de aan te roepen functie meesturen. Als child.php die dan herkent kan hij de opdracht uitvoeren. Ook nu zou main.php zichzelf aan kunnen roepen om het geheel in één bestand te houden. Merk op dat het afvangen van zo’n opdracht goed kan worden vergeleken met de controller in het MVC model. Voor geavanceerde toepassing kunnen naar wens Threads worden gemaakt die elk soort controller kunnen activeren.


 top
5. Implementatie

Deze ideeën heb ik zo volledig mogelijk geïmplementeerd. Het resultaat staat bij de scripts als multithreader , daarbij is ook een aantal voorbeelden gegeven. Verder wil ik graag een aantal adviezen geven die mij erg hebben geholpen:

  • Bouw je code vanaf het begin op in duidelijke classes en laat die overerven waar nodig. Dat zal niet alleen voor jezelf duidelijk zijn, het maakt je code ook herbruikbaar en uitbreidbaar.
  • Gebruik commentaren. Na een paar dagen aan iets anders te hebben gewerkt weet ik niet meer precies wat elke regel code doet. Het is even investeren om alles van commentaar te voorzien, maar het is de moeite waard.
  • Maak gebruik van autoloading [8], het scheelt veel als je niet telkens allerlei require statements hoeft te gebruiken.
  • Het doorgeven van argumenten aan een functie in de vorm van een array is erg makkelijk. Zo maakt het niet uit in welke volgorde je ze opgeeft en kun je ze eenvoudig met array_merge() aanvullen met vooraf gedefinieerde waarden.
  • Pas op voor oneindige lussen en de eerder genoemde vicieuze cirkel waarin steeds meer nieuwe Threads worden gestart. De server kan er zo langzaam door worden dat je het proces niet eens meer kunt beëindigen met een crash als gevolg.

Het aanroepen van een functie als thread kan met de verschillende FunctionThreads. Bij het versturen van de informatie is het belangrijk dat deze correct overkomt. Bij het starten van ProcessThreads moet worden opgepast met spaties en bij HttpThreads moet worden uitgekeken voor filters als magic quotes. Om dit te voorkomen, wordt de informatie met serialize() en base64_encode() beschermt en in de thread met de omgekeerde bewerkingen weer bruikbaar gemaakt. Steeds moet in het aangeroepen bestand worden herkend dat om een thread gaat om deze te activeren. Daarvoor moet de bijbehorende FunctionThread::activate() functie worden aangeroepen.

Curlthreads bestaan uit een losse curl sessie en een multicurlsessie waardoor deze asynchroon kunnen worden uitgevoerd. Bij het uitvoeren van een CurlThread in de CurlThreadManager wordt de multicurlsessie van de manager gebruikt, maar nu met verschillende curl sessies. De multicurlsessie van de thread zelf zal niet worden gebruikt, met als gevolg dat zijn methoden niet meer werken. Dat is onhandig en dat heb ik opgelost door de threads te blokkeren. Het probleem is dat de curlthreadmanager geen volledige toegang heeft tot elke thread, dus ik heb wat trucks in moeten bouwen zodat de benodigde functionaliteit evengoed bereikbaar is. Ik weet zeker dat hier een nettere oplossing voor moet zijn en ik hoor graag jullie advies.

Als een thread klaar is wordt de _ready() functie aangeroepen. Deze functie kan woorden gebruikt om resultaten te verwerken. Dit soort functies die worden aangeroepen voor of na een bepaalde gebeurtenissen worden ook wel hooks [9] genoemd. Dit concept is heel eenvoudig te implementeren en geeft veel vrijheid en flexibiliteit.


 top
6. Resultaten

Om nu te testen wat voor snelheidswinst multithreading op deze manier oplevert heb ik een aantal tests uitgevoerd. Daarbij wordt de volgende code uitgevoerd:

  1. function test($iSeconds = 5) {
  2.     $fTime = microtime(true);
  3.     while (microtime(true) - $iSeconds < $fTime);
  4. }

Dit is dus een oneindige lus die de processor vijf seconden lang maximaal belast om zo een extreem zware taak te simuleren. In de test moet deze code vijftig keer worden uitgevoerd terwijl er gebruik wordt gemaakt van tien Threads. Theoretisch zou dit in 50 * 5 / 10 = 25 seconden moeten kunnen worden gedaan met deze tien Threads, er van uitgaande dat de processor genoeg rekencapaciteit heeft. De snelheidswinst in procenten die is behaald ten opzicht van het serieel uitvoeren van de vijftig taken is te berekenen met de formule:

Snelheidswinst = 100 * (1 – Uitvoertijd / 250)

Daarbij is 250 natuurlijk de tijd die het zou kosten als alle taken één voor één zouden worden uitgevoerd. Het kan goed zijn dat de processor te zwaar wordt belast tijdens het uitvoeren van tien taken tegelijk en het uitvoeren langer duurt dan de theoretische vijfentwintig seconden. Ook kan het starten van nieuwe taken relatief veel tijd kosten zodat het programma vertraging oploopt. Een totale efficiëntie kan worden berekend met:

Efficiëntie = 100 * 25 / Uitvoertijd

Behalve de verschillende typen Threads zijn ook ook de snelheden van de verschillende ThreadManagers bepaald. Alle typen threads zijn uitgevoerd met de standaard ThreadManager. De Proccess- en HttpFunctionThreads zijn ook met de StreamThreadManager getest en de CurlFunctionThread ook met de CurlFunctionThreadManager. Deze tests zijn beschikbaar als voorbeeld 1 t/m 6 bij het script. Bij het testen is gebruik gemaakt van twee verschillende systemen: één met een Intel Pentium Dualcore E2180 processor en één met een Intel Core i5 750 Quadcore processor, beiden met een kloksnelheid van 3.0 GHz. Als besturingssysteem is respectievelijk gebruik gemaakt van Windows 7 x86 en Windows 7 x64. Op beide systemen wordt gebruik gemaakt van de Apache webserver op standaardinstellingen.

Alle tests zijn vijf keer uitgevoerd. In deze tabel staan de gemiddelde waarden.

Het is duidelijk dat op de quadcore betere resultaten worden behaald, maar in elke test is er snelheidswinst! Opvallend is dat de resultaten voor HttpFunctionThreads erg tegenvallen op de dualcore, maar juist perfect zijn op de quadcore. Waarschijnlijk heeft Apache een snelle processor nodig om efficiënt met veel zware taken om te gaan. ProcessFunctionThreads zijn steeds erg snel, maar het lijkt erop dat het starten van steeds een nieuw proces toch meer tijd kost dan het maken van een http request.

CurlFunctionThreads presteren redelijk en zijn onverwacht sneller dan HttpFunctionThreads. Dat is opmerkelijk, want het gebruiken van de curl extensie kost waarschijnlijk extra rekenkracht en er wordt met dezelfde webserver gewerkt. Wellicht komt de snelheidswinst omdat curl inwendig op een lager level werkt dan php. Aan de andere kant is het gebruik van de CurlThreadManager erg langzaam vergeleken met de standaard ThreadManager. Waarschijnlijk begrijp ik de werking van de multicurlsessie techniek niet goed genoeg om er optimaal gebruik van te kunnen maken.

De StreamThreadManager levert vergelijkbare resultaten als de standaard ThreadManager. Alle moeite om de originele methode met stream_select() te gebruiken lijkt voor niets, maar ook hier moet winst zijn te behalen. In de vele lussen die de Threads en ThreadManagers gebruiken wordt vaak korte tijd (standaard één milliseconde) gewacht om de processor te ontzien. Door met deze wachttijd en verschillende aantallen threads te experimenteren moet een optimale combinatie zijn te vinden waardoor maximale snelheid en efficiëntie wordt behaald.


 top
7. Conclusie

Door een uitgebreide constructie van verschillende Threads en ThreadManagers is het heel eenvoudig om de meest uiteenlopende programma’s te versnellen. De processor blijft toch de snelheidsbeperkende factor en eigenlijk moet er een controle worden ingebouwd waardoor het processorgebruik van het hoofdscript en alle threads samen onder een bepaalde grens blijft. Op die manier kan de server vloeiend blijven draaien. Ook moeten de mogelijkheden van het hergebruiken van Threads worden onderzocht. Als één thread verschillende taken serieel uitvoert scheelt dat in de tijd die het kost om steeds een nieuwe thread te starten. Ook dat moet goed beheerd worden door het hoofdscript en er moeten uitgebreidere communicatiemogelijkheden tussen hoofdscript en thread worden gecreëerd.

Uiteindelijk vallen de resultaten voor de huidige implementatie absoluut niet tegen voor een taal die niet is ontworpen om multithreading te ondersteunen. Multithreaden blijkt een strategie waarmee enorm veel tijdswinst kan worden behaald ten opzichte van het serieël uitvoeren van herhaalde taken.


 top
8. Referenties
  1. Process Control
  2. Multi-thread Simulation
  3. Improved Thread Simulation
  4. Develop multitasking applications
  5. PHP command line
  6. Client URL Library
  7. Continuously downloading
  8. Autoload
  9. Hooking

 top
9. Afsluiting

In deze tutorial heb ik geprobeerd duidelijk te maken hoe het idee achter mijn multithreader script tot stand is gekomen. Daarbij ben ik niet al te diep ingegaan op de onderliggende code. Ik moedig iedereen die geïnteresseerd is in dit onderwerp aan om mijn code eens te bekijken en verder te lezen in de artikelen die ik ter referentie heb genoemd.

Bij het testen en het interpreteren van de resultaten zijn een aantal verbeterpunten aan het licht gekomen waar ik wellicht later op terug kom.

Vragen, opmerkingen, suggesties en commentaren zijn altijd welkom in een pm of als reactie!

Rik Veenboer

 top


« Vorige tutorial : [OOP] Een begin maken met OOP Volgende tutorial : Pagina navigatie in PHP en MySQL »

© 2002-2024 Sitemasters.be - Regels - Laadtijd: 0.013s