Multiprocessing met PHP?! What the fork?!

Facebook
Twitter
LinkedIn
Reddit
Leestijd: 9 minuten

Er is veel te vinden over een single proces PHP script, maar er wordt naar mijn mening te weinig aandacht besteed aan multitasking met PHP, velen weten denk ik niet eens dat het kan. Eigenlijk moeten we het multiprocessing noemen, niet te verwarren met multithreading. Even een hele korte uitleg over de verschillen:

Er zit een verschil in multithreading en multiprocessing. Bij multithreading worden er binnen een applicatie virtuele processen (threads) aangemaakt, welke gebruik maken van de resources die de applicatie toegewezen heeft gekregen. Bij multiprocessing worden er, naast de bestaande applicatie, nieuwe processen aangemaakt met hun eigen geheugen, process-id etc. toegewezen krijgen. Sommige mensen zullen het er niet mee eens zijn, maar Unix programmeurs kijken naar multithreading met een zekere mate van wantrouwen. Unix systemen verkiezen over het algemeen multiprocessing boven multithreading. Dit heeft te maken met het feit dat het creëren van een proces (vaak “forking” of “spawning” van een “child process” genoemd) op een Unix systeem vele malen sneller gaat. Op andere besturingssystemen, zoals Windows, is forking behoorlijk langzaam en daarom is daar threading populairder.

Process Control (PCNTL) in PHP maakt gebruik van multiprocessing en is daarom niet te gebruiken op Windows platformen. Process Control ondersteuning is een onderdeel van PHP (PHP 4 >= 4.1.0, PHP 5), maar is standaard uitgeschakeld. Om deze ondersteuning in te schaken moet de CGI of CLI versie van PHP worden gecompileerd met –enable-pcntl als configuratie optie. Er hoeft niets veranderd te worden in de php.ini of iets dergelijks, het werkt meteen. Let op! Process Control werkt alleen vanaf de CLI (Command Line Interface) en kan niet worden aangesproken via een webbrowser.

In dit artikel wil ik niet al te diep ingaan op alle PCNTL functies, echter wil ik alleen onder de aandacht brengen dat ook PHP meerdere dingen te gelijk kan en hoe simpel dit eigenlijk is. Vaak zie ik PHP programmeurs scripts ontwikkelen waarbij geen gebruik wordt gemaakt van de middelen die de hardware biedt. Laatst zag ik een topic van iemand die bestanden wilde downloaden met PHP door middel van een cron job. De bedoeling was dat het script om de 3 uur bepaalde bestanden download, omdat de bestanden om de 3 uur gewijzigd worden. Dit kreeg hij echter niet voor elkaar, het script kon alle bestanden niet binnen de gestelde tijd van 3 uur downloaden. De reden hiervan was dat de bestanden te langzaam binnen kwamen. Hij moest 6 bestanden downloaden van verschillende hosts en deze hosts knepen de verbinding dusdanig dat het een uur duurde voor hij elk bestand binnen had. Hij maakte gebruik van een single proces PHP script en zijn cron job sprak het script aan door middel van “wget”. Helaas weet ik niet hoe deze discussie is afgelopen, maar het gaf mij een mooi voorbeeld om te gebruiken in dit artikel. Bij een single proces PHP script wacht het script tot de eerste taak klaar is voor hij met de tweede begint. Het downloaden van die 6 bestanden zou dus 6 uren in beslag nemen, terwijl het ook in 1 uur kan, maar hoe?

Multiprocessing in PHP maakt gebruik van de PCNTL module en kan aangesproken worden met de functie pcntl_fork(). Wanneer deze functie wordt aangeroepen geeft het 1 van de 3 waardes terug. De waardes die je kan terug verwachten van deze functie zijn:

  • -1 => Het aanmaken (“forking”) van nieuwe (“child”) processen is niet gelukt.
  • 0 => Het huidige proces is een nieuw (“child”) proces dat is aangemaakt (“forked”)
  • > 0 => Wanneer de waarde hoger dan 0 is, dan hebben we te maken met de zogenaamde “parent”, het script waar alles uiteindelijk begonnen is.

Het script wat je maakt en wat je uiteindelijk gaat draaien heet dus “parent” en de nieuwe processen die de parent gaat creëren zijn “child” processen. Het creëren van child processen heet “forking”. Dit lijkt me vrij simpel om te begrijpen en de benamingen spreken ook voor zich. Na een succesvolle fork heb je twee kopieën van PHP die het zelfde script uitvoeren op exact het zelfde moment. Beide zullen verder gaan vanaf de lijn waar de functie pcntl_fork() is aangesproken. Een child proces erft alle variabelen van haar parent en ook haar resources. Een belangrijk item wat veel mensen vergeten is dat een kopie van een resource geen unieke resource is, ze verwijzen beide naar het zelfde punt en dit kan problemen veroorzaken, maar hier kom ik later op terug. Ik wil nu eerst de basis van pcntl_fork() behandelen.

<?php
$iPid = pcntl_fork();

switch($iPid)
{
case -1:
die("Could not fork!");
case 0:
echo "I am the child!";
break;
default:
echo "I am the parent!";
break;
}
?>

Het script hierboven laat alleen de tekst zien van zowel de child als de parent, maar laat niet zien dat de variabelen van het script ook zijn overgenomen door de children. Probeer daarom het volgende script uit om te zien dat de data wordt overgenomen door de children. Noem het script bijv. test.php en draai het op de command line van linux door middel van het commando “php test.php”.

<?php
for($i = 1; $i <= 5; $i++)
{
$iPid = pcntl_fork();

switch($iPid)
{
case -1:
die("Could not fork!");
case 0:
sleep(1);
echo"In child {$i}\n";
exit;
}
}
?>

In het bovenstaande voorbeeld zien we hoe 5 children worden creëert en dat elk child de variabele $i kan aanspreken. Dit kan, omdat elke child een kopie van de alle data mee krijgt. Het bovenstaande script zal “In child 1”, “In child 2”, “In child 3”, “In child 4” en “In child 5” weergeven in je terminal echter is het niet zo simpel als dat het lijkt. Kijk eens goed naar het script, na elke echo wordt er een exit gegeven. Normaal gesproken zou hier het script meteen moeten stoppen en dat doet het hier ook! Toch krijg je alle tekst te zien in je terminal scherm en dit komt omdat het aanspreken van de exit() functie alleen het child PHP script stopt en niet de parent of de andere children. Dit voorbeeld laat duidelijk zien dat er echt verschillende processen ontstaan. Echter kan je uit dit voorbeeld meer feitelijke informatie halen.

Zeer waarschijnlijk kreeg je de tekst “In child 1”, “In child 2”, “In child 3”, “In child 4” en “In child 5” te zien en ook in deze volgorde, dit is meestal het geval, maar het kan ook zijn dat de volgorde anders is met het zelfde script. Je kan dus niet vertrouwen op de volgorde waarin de children worden uitgevoerd. Dit is 1 van de basisprincipes van multiprocessing: zodra je een child proces forked, bepaald je OS wanneer deze wordt uitgevoerd en hoeveel tijd de child krijgt. Wellicht is het ook opgevallen dat je meteen na het uitvoeren van de parent meteen weer op de shell prompt terug kwam?! Hoewel de children gekoppeld zijn aan de terminal, zijn ze in wezen op de achtergrond. Zodra het parent proces wordt gestopt, krijg je je shell prompt (dus de controle) meteen terug en kan je andere applicaties starten, terwijl de children op de achtergrond draaien. Zonder de sleep() functie was dit misschien niet zo opgevallen. Elk child proces leeft dus een eigen leven, maar zoals in het echte leven kan ook in PHP het parent proces een oogje in het zeil houden. Echter wil hier nu niet op in gaan aangezien ik anders mijn doel van een “simpele uitleg” voorbij schiet (wellicht in een ander artikel ooit eens).

We weten nu dus hoe we unieke processen kunnen creëren, dus kunnen we aan de slag met het voorbeeld in dit artikel. Het downloaden van bestanden binnen een gestelde tijd, elke download krijgt een eigen proces zodat alle downloads tegelijkertijd klaar kunnen zijn, zonder dat ze op elkaar hoeven te wachten. Ook heeft het parent proces geen functie meer zodra alle children er zijn, dus deze mag ook afsluiten. Ik ga de functies voor het downloaden niet volledig uitschrijven aangezien daar het artikel niet om gaat, wel wil ik zo nog even ingaan om het delen van resources van parent naar child.

<?php
$aUrl = array();
$aUrl[] = 'http://www.voorbeeld.nl/bestand_1.php';
$aUrl[] = 'http://www.voorbeeld.nl/bestand_2.php';
$aUrl[] = 'http://www.voorbeeld.nl/bestand_3.php';
$aUrl[] = 'http://www.voorbeeld.nl/bestand_4.php';
$aUrl[] = 'http://www.voorbeeld.nl/bestand_5.php';
$aUrl[] = 'http://www.voorbeeld.nl/bestand_6.php';

$i = 1;
foreach($aUrl AS $sUrl)
{
$iPid = pcntl_fork();

switch($iPid)
{
case -1:
die("Could not fork!");
case 0:
DownloadFile($sUrl);
exit;
default:
if($i == count($aUrl))
{
die(" * All children are alive!\n");
}
else
{
continue;
}
}

$i++;
}
?>

Allereerst, mijn excuses dat de PHP source code niet netjes is uitgelijnd, ik heb nog geen plug-in kunnen vinden voor WordPress die dit netjes doet en ik heb hier zelf nog geen tijd voor gehad. Zoals ik al aangaf ontbreekt de “DownloadFile()” functie, maar goed, deze zou je zelf kunnen invullen met een bepaalde functie. In het voorbeeld hierboven zie je wederom dat ik variabelen van het parent proces mee neem naar de children door middel van een foreach loop. Er loopt ook een counter mee en ook dit heeft een reden. De counter $i houdt in de gaten hoeveel child processen er zijn gemaakt, zodra ze er allemaal zijn, zal de parent zichzelf afsluiten met de tekst ” * All children are alive!”. Alle downloads die in dit voorbeeld gestart worden, worden gestart in een eigen afzonderlijk proces en kunnen deze tegelijkertijd naast elkaar downloaden en zullen dus ook tegelijkertijd klaar zijn, binnen de gestelde 3 uur. Zo simpel is het dus, de basics van PCNTL forking!

Toch wil ik nog even ingaan op die resources die een child kopieert van haar parent. Een child maakt dus geen nieuwe resource aan maar kopieert die van haar parent. Makkelijk zou je zeggen, dat scheelt weer resources, maar schijn bedriegt! Dit is waar de meeste mensen in het rijk van het onbekende komen, het erven van resources zorgt voor meer problemen dan dat het oplost. Hier een voorbeeld code:

<?php
$rHandle = mysql_connect("localhost", "user", "password");
mysql_select_db("database", $rHandle);

mysql_query("INSERT INTO `forktest` VALUES ('This parent is about to fork children')", $rHandle);

for($i = 1; $i <= 5; $i++)
{
$iPid = pcntl_fork();

switch($iPid)
{
case -1:
die("Could not fork!");
case 0:
echo $rHandle."\n";
mysql_query("INSERT INTO `forktest` VALUES ('In child $i')", $rHandle);
exit;
}
}
?>

Maak voor het gemak even een database aan met daarin een tabel die “forktest” heet. In deze tabel maken we een veld aan met een willekeurige naam varchar(255). Volgens PHP zou de resource moeten worden gedeeld met de children. Veel mensen zeggen dat dat niet gebeurd en dat dit een bug in PHP of MySQL is, maar naar mijn idee gebeurd dit wel. PHP kopieert netjes haar resource naar haar child proces, dit kan je zien door de echo van $rHandle. Bij mij ging het om “Resource id #4” en deze echo kreeg ik 5 keer keer, zoals je ook mag verwachten, gezien het feit er 5 children zijn. Toch krijg ik tussendoor ook af en toe verschillende meldingen in mijn terminal te zien, zoals “MySQL server has gone away” of “Lost connection to MySQL server during query”. Overal waar je op internet kijkt zegt iedereen “Dit is een bug in PHP!” of “MySQL ondersteund dit niet!” etc. etc. kortom, niemand weet precies hoe dit kan, hoewel het volgens mij “Not Exactly Rocket Science” is. Nogmaals, je OS bepaald in welke volgorde en wanneer je child aan de beurt is. Zodra je in PHP de functie exit() aanspreekt of zodra je proces stopt om welke reden dan ook, dan sluit hij autom. de verbinding met MySQL. Dit zodra je parent stop (je shell prompt terug geeft) of zodra 1 van je child processen aan haar einde is, wordt de verbinden van (in mijn geval) Resource id #4 gesloten en aangezien elke child deze resource heeft gekopieerd, kunnen zij geen verbinding meer maken terwijl ze wel een resource id hebben. MySQL geeft normaal gesproken de melding “Warning: mysql_query(): 4 is not a valid MySQL-Link resource in…” als een resource met mysql_close() is gesloten. Dit is denk ik de reden dat veel mensen de meldingen “MySQL server has gone away” of “Lost connection to MySQL server during query” niet kunnen plaatsen en daarom denken dat het een bug is, echter is het een logisch gevolg van een logische gebeurtenis.

Een oplossing voor het erven van resources? Gewoon niet doen! Als je je resource nodig hebt voordat er children geforked worden, sluit deze resource dan ook voor er children geforked worden! Mocht je je resource ook nodig hebben in elke child proces, maak dan in je child een nieuwe connectie aan. Dit maakt je code duidelijk en hiermee vermijdt je dat processen op elkaars tenen gaan staan. Ook vermijdt je hier mee veel cross-platform problemen met je code.

Process Control in PHP kan ook worden gebruikt wanneer je bijvoorbeeld een systeem monitoring script maakt of een chat server. Mochten er lezers zijn die interesse hebben bij een vervolg op dit document waarbij dieper wordt ingegaan op de PCNTL functies, laat het dan even weten! Laat ook weten wat je van dit artikel vind!

Geef een reactie

Je e-mailadres wordt niet gepubliceerd. Vereiste velden zijn gemarkeerd met *

Deze site gebruikt Akismet om spam te verminderen. Bekijk hoe je reactie-gegevens worden verwerkt.