Multithreading in PHP18 Jul

Von Julian am 18.07.2009. Es wurden 2 Kommentare hinterlassen. Du kannst einen Kommentar hinterlassen oder trackbacken.

PHP unterstützt by default kein Multithreading und wird es auch nicht in nächster Zukunft. Wikipedia klärt auf, was mit Multithreading eigentlich genau gemeint ist.
Was ich im Nachfolgenden darstellen möchte, ist kein “echtes” Multithreading, da dieses nach Definition innerhalb eines Prozesses abläuft. Durch einen Trick ist es aber möglich mehrere Ergebnisse parallel zu berechnen. Allerdings wird man dabei etwas eingeschränkt.

Anton Vedeshin und einige andere haben bereits verschiedene Lösungen zu diesem Thema dargestellt. Einen sehr interessanten Ansatz, den ich leider nicht testen konnte, ist auch der von Mike´s Blog. Darin wird eine nur Linux-kompatible Funktion verwendet.

Entgegen der vielen Behauptungen “Multithreading” sei nicht möglich, habe ich eine Lösung von Guxx.de modifiziert und mich durch vorwiegend englische und französische Blogs gelesen.

Die Idee ist, eine Aufgabe, die parallel (und mehrfach) bearbeitet werden kann, in eine externe Datei auszulagern. Diese externe Datei wird dann mit popen() aufgerufen. Popen() wartet – wieder entgegen vieler Behauptungen in Kommentaren zu diesen Themen – nicht so lange, bis der externe Prozess abgearbeitet ist, sondern gibt nur ein Handle, mit dem man das Ergebnis des Prozesses auslesen kann. Nähere Informationen finden sich dazu in der php-Dokumentation.

Nun wollen wir aber nicht ein Prozess, sondern mehrere aufrufen und beweisen, dass sie parallel und voneinander unabhängig arbeiten. Guxx.de hat das mit einem Beispiel dargestellt, das Suchergebnisse von verschiedenen Suchmaschinen ausliest. Ich habe das übernommen und etwas aktualisiert.
Zunächst haben wir einige Suchmaschinen, die wir aufrufen möchten:

$engines = array('g1' => 'http://www.google.de/search?hl=de&btnG=Google-Suche&meta=&q=',
                 'y1' => 'http://de.search.yahoo.com/search?fr=yfp-t-501&ei=UTF-8&rd=r1&p=',
                 'b1' => 'http://www.bing.com/search?go=Suche&mkt=de-de&scope=&FORM=LIVSOP&q=');

Ich habe jeder URL einen Index gegeben, um später ein bestimmtes Log mitlaufen zu lassen (dazu später).
Jeder dieser URLs muss nun einfach nur ein Suchbegriff angehängt werden und wir erhalten die Ergebnisseite der entsprechenden Suchmaschine.

Nun brauchen wir dreierlei Variablen:

$searchkey = 'php';
$pathtocli = 'E:\xampp\php53\php';
$pathtoscript = "work.php";

Das erste ist – unduchschaubarerweise – das Suchwort. $pathtocli muss den Pfad zu CLI-beinhalten. Dies ist bei mir auf Grund der Verwendung von xampp und php5.3 “E:\xampp\php53\php”. Bei einer normalen xampp-Installation ist es für gewöhnlich “C:\xampp\php\php” – das php-Verzeichnis mit einem angehängten “\php”!
$pathtoscript letztendlich ist mein parallel laufendes Script, das die Suchergebnisse holt (liegt im selben Verzeichnis, daher reicht der Name).

Nun müssen wir im Prinzip nur die einzelnen Suchmaschinen ansprechen. Das geschieht mit einem Aufruf unserer semi-”Threads”:

foreach($engines as $key => $engine){
        $fh[$key] = popen("$pathtocli $pathtoscript \"$engine$searchkey\"", 'r' );
}

Wir führen mit php-CLI also einfach einen Prozess aus ($pathtoscript) und übergeben dabei ein einziges Argument: $engines.$searchkey

Im Grunde genommen ist das schon der Kern des Multithreadings – es laufen nun drei Prozesse parallel ab und können Dinge berechnen oder Speichern o.ä. . Ich möchte nun aber die Ergebnisse in unserer Hauptdatei angezeigt bekommen und zudem noch beweisen, dass die Prozesse parallel laufen.

Dazu gehe ich auf work.php ein:
Ich fordere zuerst den Inhalt der entsprechenden Suchmaschine an.

$result = @file_get_contents($argv[1]);

$argv[1] ist einfach die oben übergebene Suchmaschine (bzw. deren URL). Ich erhalte nun – bei entsprechenden Rechten des Webservers – den Quellcode von bspw. Google geliefert. Diesen interpretiere ich nun einfach an Hand von regulären Ausdrücken und gebe die Links aus:

$pattern = array();
$pattern[] = "/<h3\ class=r><a\ href=\"(http:\/\/.*?)\"\ class=l/";     // Pattern für Google Ergebnisse
$pattern[] = "/class=yschttl href=\".*?\/\*\*(.*?)\"/";                          // Pattern für Yahoo Ergebnisse
$pattern[] = "/<h3><a\ href=\"(.*?)\"/";                        // Pattern für die Live-Suche

Die Ausdrücke sind leider nicht perfekt, aber ich hatte keine Lust sie weiter zu verfeinern – das Beispiel soll nur die Idee erläutern.

Jetzt wird einfach an Hand der Pattern der Quellcode “analysiert”:

foreach ($pattern as $pkey => $spattern){
        if (preg_match_all($spattern, $result, $matches)){
                if (count($matches[1]) > 0){
                        foreach($matches[1] as $key => $url){
                                echo htmlentities("$url\n");
                        }
                }
        }
}

Wenn preg_match_all zutrifft und $matches[1] ein Array ist (siehe in der php-Dokumentation unter preg_match_all), dann wird die URL mit einem endenden “\n” ausgegeben. Dieses Zeichen verwende ich später in der Hauptdatei wieder, um die Ergebnisse zu trennen.
Ich erhalte nun also – je nach Suchmaschine – unterschiedliche Treffer. Jetzt könnte ich diese entweder in einer DB speichern oder eben wieder mit dem Hauptscript auslesen:

$links = array();
foreach($fh as $key => $handle){
        $result = "";
        while (!feof($handle)){
                $result .= fgets($handle);
        }
        $links[$key] = explode("\n", $result);
}

Jedes Handle wird einfach ausgelesen und an Hand von \n getrennt. Das ergibt ein Array unserer Ergebnisse, die in $links gespeichert werden. Hier muss man beachten: Dies läuft nicht mehr parallel! Das Hauptscript kann durch die foreach-Schleife nur in der oben angegebenen Reihenfolge der Suchmaschinen-URLs Daten der parallelen Prozesse auslesen. Zwar sind die Ergebnisse zum Teil schon früher fertig, aber an dieser Stelle braucht das Hauptscript so lange, wie der längste Prozess (also sollten diese möglichst gleichmäßig und schnell arbeiten).
Eine Ausgabe der Ergebnis-Links erfolgt einfach über:

print_r($rar);

Nun konnte ich aber noch nicht beweisen, dass die Prozesse parallel ablaufen. Das mache ich mit einem ganz primitiven Log-Script:

$searchkey = 'php';
$pathtocli = 'E:\xampp\php53\php';
$pathtoscript = "work.php";
 
$engines = array('g1' => 'http://www.google.de/search?hl=de&btnG=Google-Suche&meta=&q=',
                 'y1' => 'http://de.search.yahoo.com/search?fr=yfp-t-501&ei=UTF-8&rd=r1&p=',
                 'b1' => 'http://www.bing.com/search?go=Suche&mkt=de-de&scope=&FORM=LIVSOP&q=',
                 'b2' => 'http://www.bing.com/search?go=Suche&mkt=de-de&scope=&FORM=LIVSOP&q=',
                 'y2' => 'http://de.search.yahoo.com/search?fr=yfp-t-501&ei=UTF-8&rd=r1&p=',
                 'b3' => 'http://www.bing.com/search?go=Suche&mkt=de-de&scope=&FORM=LIVSOP&q=',
                 'y3' => 'http://de.search.yahoo.com/search?fr=yfp-t-501&ei=UTF-8&rd=r1&p=',
                 'y4' => 'http://de.search.yahoo.com/search?fr=yfp-t-501&ei=UTF-8&rd=r1&p=',
                 'b4' => 'http://www.bing.com/search?go=Suche&mkt=de-de&scope=&FORM=LIVSOP&q=',
                 'g2' => 'http://www.google.de/search?hl=de&btnG=Google-Suche&meta=&q=',
                 'y5' => 'http://de.search.yahoo.com/search?fr=yfp-t-501&ei=UTF-8&rd=r1&p=',
                 'b5' => 'http://www.bing.com/search?go=Suche&mkt=de-de&scope=&FORM=LIVSOP&q=',
                 'g3' => 'http://www.google.de/search?hl=de&btnG=Google-Suche&meta=&q=',
                 'g4' => 'http://www.google.de/search?hl=de&btnG=Google-Suche&meta=&q=');
 
foreach ($engines as $key => $engine){
        file_put_contents('info.txt', "[".time()."] ".$engine." called\r\n", FILE_APPEND);
        $fh[$key] = popen("$pathtocli $pathtoscript \"$engine$searchkey\"", 'r' );
}
 
$links = array();
foreach($fh as $key => $handle){
        $result = "";
        while (!feof($handle)){
                $result .= fgets($handle);
        }
        $links[$key] = explode("\n", $result);
        file_put_contents('info.txt', "[".time()."] got result of key ".$key."\r\n", FILE_APPEND);
}
 
print_r($links);

Ich habe im Hauptscript (das so fertig sein, aber nur als Beispiel dienen sollte!) zwei file_put_contents()-Aufrufe eingefügt. Diese speichern in “info.txt” die Zeit und eine kleine Info.
In der Datei “work.php” füge ich ebenso noch zwei kleine Dinge ein:

$result = @file_get_contents($argv[1]);
 
$pattern[] = "/<h3\ class=r><a\ href=\"(http:\/\/.*?)\"\ class=l/";     // Pattern für Google Ergebnisse
$pattern[] = "/class=yschttl href=\".*?\/\*\*(.*?)\"/";                          // Pattern für Yahoo Ergebnisse
$pattern[] = "/<h3><a\ href=\"(.*?)\"/";                        // Pattern für die Live-Suche
 
 
foreach ($pattern as $pkey => $spattern){
        if (preg_match_all($spattern, $result, $matches)){
                if (count($matches[1]) > 0){
                        foreach($matches[1] as $key => $url){
                                echo htmlentities("$url\n");
                        }
                }
        }
}
 
$add = "";
if(strpos($argv[1], "bing")){
	Sleep(2);
	$add = ", slept 2 secs";
}
if(strpos($argv[1], "google")){
	Sleep(3);
	$add = ", slept 3 secs";
}
file_put_contents('info.txt', "[".time()."] ".$argv[1]." returns".$add."\r\n", FILE_APPEND);

Nun erhalte ich bei einem Aufruf meines Hauptscripts folgende Logdatei:

[1247568126] http://www.google.de/search?hl=de&btnG=Google-Suche&meta=&q= called
[1247568126] http://de.search.yahoo.com/search?fr=yfp-t-501&ei=UTF-8&rd=r1&p= called
[1247568126] http://www.bing.com/search?go=Suche&mkt=de-de&scope=&FORM=LIVSOP&q= called
[1247568126] http://www.bing.com/search?go=Suche&mkt=de-de&scope=&FORM=LIVSOP&q= called
[1247568126] http://de.search.yahoo.com/search?fr=yfp-t-501&ei=UTF-8&rd=r1&p= called
[1247568126] http://www.bing.com/search?go=Suche&mkt=de-de&scope=&FORM=LIVSOP&q= called
[1247568126] http://de.search.yahoo.com/search?fr=yfp-t-501&ei=UTF-8&rd=r1&p= called
[1247568126] http://de.search.yahoo.com/search?fr=yfp-t-501&ei=UTF-8&rd=r1&p= called
[1247568127] http://www.bing.com/search?go=Suche&mkt=de-de&scope=&FORM=LIVSOP&q= called
[1247568127] http://www.google.de/search?hl=de&btnG=Google-Suche&meta=&q= called
[1247568127] http://de.search.yahoo.com/search?fr=yfp-t-501&ei=UTF-8&rd=r1&p= called
[1247568127] http://www.bing.com/search?go=Suche&mkt=de-de&scope=&FORM=LIVSOP&q= called
[1247568127] http://www.google.de/search?hl=de&btnG=Google-Suche&meta=&q= called
[1247568127] http://www.google.de/search?hl=de&btnG=Google-Suche&meta=&q= called
[1247568128] http://de.search.yahoo.com/search?fr=yfp-t-501&ei=UTF-8&rd=r1&p=php returns
[1247568129] http://de.search.yahoo.com/search?fr=yfp-t-501&ei=UTF-8&rd=r1&p=php returns
[1247568129] http://de.search.yahoo.com/search?fr=yfp-t-501&ei=UTF-8&rd=r1&p=php returns
[1247568129] http://de.search.yahoo.com/search?fr=yfp-t-501&ei=UTF-8&rd=r1&p=php returns
[1247568129] http://de.search.yahoo.com/search?fr=yfp-t-501&ei=UTF-8&rd=r1&p=php returns
[1247568130] http://www.bing.com/search?go=Suche&mkt=de-de&scope=&FORM=LIVSOP&q=php returns, slept 2 secs
[1247568130] http://www.bing.com/search?go=Suche&mkt=de-de&scope=&FORM=LIVSOP&q=php returns, slept 2 secs
[1247568131] http://www.google.de/search?hl=de&btnG=Google-Suche&meta=&q=php returns, slept 3 secs
[1247568131] got result of key g1
[1247568131] got result of key y1
[1247568131] got result of key b1
[1247568131] got result of key b2
[1247568131] got result of key y2
[1247568131] http://www.bing.com/search?go=Suche&mkt=de-de&scope=&FORM=LIVSOP&q=php returns, slept 2 secs
[1247568131] got result of key b3
[1247568131] got result of key y3
[1247568131] got result of key y4
[1247568131] http://www.bing.com/search?go=Suche&mkt=de-de&scope=&FORM=LIVSOP&q=php returns, slept 2 secs
[1247568132] http://www.google.de/search?hl=de&btnG=Google-Suche&meta=&q=php returns, slept 3 secs
[1247568132] http://www.google.de/search?hl=de&btnG=Google-Suche&meta=&q=php returns, slept 3 secs
[1247568132] http://www.bing.com/search?go=Suche&mkt=de-de&scope=&FORM=LIVSOP&q=php returns, slept 2 secs
[1247568132] got result of key b4
[1247568132] got result of key g2
[1247568132] got result of key y5
[1247568132] got result of key b5
[1247568132] http://www.google.de/search?hl=de&btnG=Google-Suche&meta=&q=php returns, slept 3 secs
[1247568133] got result of key g3
[1247568133] got result of key g4

Wie ist dieses zu interpretieren?
Zum Zeitpunkt 1247568126 werden zunächst alle Prozesse nach angegebener Reihenfolge im Array aufgerufen. Allerdings folgt nun schon das Durcheinander / die Parallelisierung: Yahoo antwortet als erste Suchmaschinen mit allen Anfragen (ich habe 4 gestartet). Das liegt daran, dass ich bei Google und Bing jeweils ein Delay von 3 und 2 Sekunden eingebaut habe (siehe: Sleep()). Es folgen zwei Ergebnisse von Bing und schließlich eines von Google. Da Google der erste Eintrag der Suchmaschinen ist, wartet das Hauptscript in der Abfrage logischerweise, bis dieses Ergebnis vorliegt. Damit wird dort keine Parallelisierung umgesetzt, außerhalb des Hauptscriptes arbeiten aber alle unabhängig. Nun werden alle Anfragen der Reihe nach abgearbeitet – wenn ein Ergebnis noch nicht vorliegt, muss gewartet werden.

Damit wäre eine Parallelisierungsmöglichkeit in PHP bewiesen. Das Ganze kann man jetzt noch sehr vielfältig verfeinern: Zunächst einmal könnte man die Ergebnisse nicht wieder im Hauptscript auslesen, sondern einfach über den jeweiligen Prozess in einer Datenbank speichern lassen. Diese Einträge könnte man wiederum im Hauptscript auslesen.
Umfangreicher kann das Ganze noch werden, wenn man verschiedene Dateien für den Prozessaufruf verwendet. Beispielsweise eine Datei, die nur Yahoo-Daten ausliest (und mehrmals aufgerufen wird) und eine, die bspw. nur Google-Daten ausliest.
Daten, die in einer Datenbank gespeichert wurden, könnten z.B. im Hauptscript per AJAX nach und nach geladen werden. Das sähe sicher ganz interessant aus, wenn nacheinander die vielen Ergebnisse aufploppen würden – und dabei ist es völlig egal, ob sie von Yahoo, Google oder Bing kommen!

Ich würde mich sehr über Kommentare freuen!

lG

2 Kommentare zu "Multithreading in PHP"

  1. krisi sagt:

    Hey Julian,

    das sieht ja schonmal ganz gut aus soweit.

    Wenn ich das jetzt mal weiterspinne, dann würde das heißen, dass man (und das wäre insbesondere für GI sehr interessant) unabhängige und ähnliche Bearbeitungsroutinen zB für die Berechnung einer Karte o.ä. mit PHP problemlos parallelisieren kann. Garnichtmal so übel. und da dein Script für jeden “threat” den PHP-Parser neu lädt, müsste das Betriebsystem theoretisch auch in der Lage sein, diese Prozesse auf mehrere Prozessoren verteilen zu können. Hört sich echt geil an. Super Arbeit!

  2. Julian sagt:

    Übrigens hängt dieser ganze Spaß vom Webserver ab! Denn dieser entscheidet, ob die aufgerufenen Prozesse wirklich eigene Prozesse auf dem Server sind. Bei Apache ist das so, andere Webserver lassen aber vieles nicht in jeweils einem eigenen Prozess laufen, wodurch eine solche Parallelisierung selbstverständlich nicht stattfindet. Ein interessanter und lustiger Test, aber damit auch wohl weniger praktikabel!

Hinterlasse einen Kommentar

© 2009 GlobalIndustry-Project Blog