=encoding utf-8 =head1 Threads in dynamischen Sprachen und warum Perl-Pseudo-Threads sterben sollten Wie man am Titel sieht, soll es im folgenden um zwei verwandte Themen gehen. Einerseits möchte ich grundsätzliche Gedanken über Threads liefern, einige Vorurteile ausräumen und erläutern, wie Threads in dynamischen Sprachen implementiert werden und warum dies so ist. Andererseits möchte ich erläutern, warum Perl keine Threads unterstützt und weshalb Perls sogenannte Threads eher sterben sollten, da sie mehr Schaden anrichten als sie helfen. Letzteres muss nicht einmal zu Inkompatibilitäten führen: da Perl-Threads keine sind, lassen sie sich effizient ohne emulieren. =head2 Was ist ein Thread - Eine Begriffsbestimmung Am einfachsten erklärt man Threads, in dem man sie Prozessen gegenüberstellt: Prozesse haben (üblicherweise) einen eigenen Adressraum, d.h. Variablen existieren als Kopie in jedem Prozess und sind unabhängig voneinander. Außerdem besitzt ein Prozess eine "Continuation", d.h. im wesentlichen den aktuellen "Ort" der Programmausführung. Unter Unix kommen dann noch andere Ressourcen wie File-Descriptoren hinzu, aber im wesentlichen ist es das. Ein Thread nun ist im wesentlichen ein abgespeckter Prozess (daher werden sie auch oft "LWPs" - Light Weight Processes genannt), bei dem eine oder mehrere dieser Ressourcen gemeinsam genutzt werden, insbesondere der Adressraum. Daher kann man Threads auch als Untereinheit von Prozessen ansehen: ein Prozess kann aus ein oder mehreren Threads bestehen, die sich alle den gleichen Adressraum (die gleichen Variableninhalte) teilen. Bei Threads gibt eine Menge verschiedene Variationen. Die wichtigsten sind: =over 4 =item Kooperative Threads Die "Urform" der Threads - diese Threads werden "kooperativ" genannt, nicht, weil sie es sind, sondern, weil sie es sein müssen: Sobald ein Thread ausgeführt wird, läuft dieser ohne Unterbrechung, bis er freiwillig die CPU an einen anderen Thread abgibt. Tut ein Thread dies nicht, läuft nichts anderes im System. Manchmal werden Kooperative Threads auch Koroutinen, Fibers oder Continuations genannt, was genau genommen falsch ist, aber im wesentlichen bezeichnen diese Begriffe dasselbe. Die Hauptvorteile von kooperativen Threads ist die vergleichsweise einfache Programmierung (Race-conditions sind wesentlich einfacher zu vermeiden) und ihre hohe Effizienz. Hauptnachteil ist, daß ein einzelner wildgewordener Thread das gesamte System lahmlegen kann. Windows 3.11, das Oberon-System oder das Coro-Modul von Perl sind Beispiele für kooperatives Threading. =item Preemptive Threads Um das Problem der Monopolisierung des Rechners durch einen unkooperativen (weil z.B. ferhlerhaften) Thread zu umgehen, wurden "preemptive" Threads erfunden: Diese werden ohne ihr Zutun regelmäßig "unterbrochen" (z.B. durch einen Timer) und dadurch von der CPU "verdrängt" (preempted). Diese Art von Threads wird oft auch "time-slicing" (Zeitscheibenverfahren) genannt, da hierbei ein Thread üblicherweise eine bestimmte Zahl von Zeiteinheiten erhält und, wenn er in dieser Zeit die CPU nicht freigibt, wird er automatisch verdrängt. Hauptvorteil von preemptiven Systemen ist, daß man wildgewordene Programme abbrechen kann, d.h. sie legen nicht das gesamte System lahm. Hauptnachteil ist, daß ihre Benutzung sehr kompliziert ist: auch Experten machen regelmäßig Fehler und erzeugen Race-Conditions. Neuere Windows-Versionen, alle Unix-Versionen und insbesondere alle Thread-Systeme dynamischer Sprachen (d.h. Ruby, Python usf.) benutzen dieses Verfahren. =item "Kernel-Threads" und "Userspace-Threads" Ein weiterer wichtiger Unterschied zwischen Thread-Systemen ist, ob sie vom Betriebssystem selbst implementiert werden oder innerhalb eines Prozesses. In letzterem Fall teilen sich alle Threads dieselbe Prozess-Continuation, was nichts anderes bedeutet, als daß diese Threads niemals parallel laufen können (z.B. auf einem SMP-System). Der Begriff "Kernel-Thread" bezeichnet im Gegensatz dazu die Variante, bei er die Threads tatsächlich parallel/gleichzeitig arbeiten können, obwohl dies nicht zwangsweise im "Kernel" implementiert werden muss. Userspace-Threads sind im allgemeinen um ein vielfaches schneller als Kernel-Threads - die Userspace-Threading-Bibliothek, die von Coro benutzt wird schaltet mehr als 100 mal schneller zwischen Threads um als der Linux-Kernel. Selbst auf Perl-Ebene schalten die Userspace-Threads von Coro mehr als 14 mal schneller als die Linux-Kernel-Threads. Kernel-Threads verschärfen gegenüber Userspace-Threads die Problematik der Programmierung nochmals. Ein gutes Beispiel ist MythTV, ein C++-Programm, daß Threads benutzt: auf Single-Core-Systemen arbeitet es meist korrekt, auf Multi-Core-Systemen sind crashes aber an der Tagesordnung, da der Programmierer offenbar nicht damit rechnet, daß Speicherzugriffe nicht mehr atomar sind bzw. die Gefahr einer Race-Condition stark erhöht ist. =back =head2 Wozu wurden Threads erfunden, wozu eignen sie sich? =head3 Single-Cores Multi-Core und Multiprozessorsysteme sind erst seit kurzer Zeit verbreitet. Die überwiegende Mehrheit der Rechner besitzt nach wie vor nur einzelne CPUs. Moderne CPUs besitzen im allgemeinen spezielle Hardware, um Prozesse zu unterstützen, z.B. eine Memory-Management-Unit (MMU) um den Adressraum zu verwalten und einen Translation-Lookaside-Buffer (TLB) um das ganze zu beschleunigen. Da jeder Prozess seinen eigenen Adressraum besitzt, muss die MMU bei jedem Umschalten neu konfiguriert werden, Caches wie der TLB gehen häufig verloren. Dies kann die Performance empfindlich beeinflussen, da die meisten CPUs nur einen Zustand im Cache behalten können. Die natürliche Lösung gegen zu viele Rekonfigurationen sind weniger Rekonfigurationen bei Prozesswechseln. Wechselt man den Adressraum nicht, so erhält man Threads auf natürliche Weise, da die CPU einfach den alten Adressraum weiter benutzt. Daher stammt der Name "Light Weight Processes" (leichtgewichtige Prozesse): Threads werden hier als Prozesse mit weniger "Zustand" behandelt. Dies beschleunigt das Prozessumschalten erheblich, vor allem auf älteren Systemen. =head3 Multiprozessor- oder Multi-Core-Systeme Ein weit verbreiteter Irrtum ist es, daß Threads sich gut für die Parallelisierung eignen, d.h. echte Parallelverarbeitung auf einem Multiprozessorsystem. Dies ist allerdings nicht der Fall: Das Problem, daß es nur eine Instanz der Caches oder MMU gibt, existiert bei mehreren CPU-Kernen nicht, da diese alle ihren eigenen Cache besitzen. Im Gegenteil: Dadurch, daß Threads viele Ressourcen gemeinsam nutzen, muss das Betriebssystem durch aufwändige (und langsame) Verwaltung sicherstellen, daß auch alle CPUs im System den gleichen Zustand haben. Wenn z.B. ein Prozess mit mehreren Threads auf einem Multi-Core-System arbeitet und der eine Thread vom Betriebssystem Speicher anfordert, so muss das Betriebssystem synchron alle anderen CPUs anhalten, ihnen den neuen Zustand mitteilen, warten, bis alle den Zustand übernommen haben und kann erst dann weiterarbeiten. Auf einem Dual-Core-System mag dies noch akzeptabel sein, bei mehr Cores wird dies aber schnell sehr aufwändig, auf Mehrfachprozessorsystemen, bei denen die Kommunikation zwischen CPUs noch länger braucht, wird dies schnell inakzeptabel. Aber auch der ganze normale Speicher-Cache wird nicht effizient genutzt: Da Threads alle Variablen gemeinsam nutzen, liegen gemeinsam genutzte Variablen nicht im lokalen Cache einer CPU, im Gegenteil, sie wandern häufig zwischen den Caches hin- und her, d.h. die Kommunikation läuft über den (vergleichsweise langsamen) Hauptspeicher ab. Letzteres wird häufig dadurch verhindert, daß man unterschiedliche Speicherbereiche in jedem Thread verwendet. Allerdings wird dies durch eine zusätzliche Indirektion bei jedem Zugriff bezahlt. Perls, die diese Methode benutzten, werden dadurch zwischen 15 und ca. 200% langsamer als Perls, die dies nicht tun (auch ohne Parallelbetrieb). Ein Vorteil von Threads ist die vergleichsweise schnelle Kommunikation über den Speicher, was bei Programmen, die viel Kommunikation betreiben, einen großen Gewinn bedeutet, wenn die alternative z.B. Kommunikation über Sockets ist. Moderne Betriebssysteme erlauben es aber ausnahmslos, einzelne Speicherbereiche gemeinsam zu nutzen, so daß Prozesse mit selektiv gemeinsam genutzten Speicher wesentlich effizienter auf Mehr-CPU-Systemen arbeiten, als Threads. =head3 Ein Benchmark: Matrixmultiplikation Am folgenden "Benchmark" wird dies deutlich. Das Benchmark-Programm implementiert eine parallel Matrixmultiplikation, bei der einzelne Zeilen und Spalten an vier "Arbeits-Threads" gesendet werden, die eine Skalarmultiplikation durchführen und an einen Ergebnis-Thread senden. Dabei kann es Perl-Pseudo-Threads (die parallel arbeiten) und Coro-Threads benutzen. Das Programm findet sich unter F, ist aber für die Diskussion nicht relevant. =begin latex \begin{center} \includegraphics{mat2.eps} \end{center} =end latex =head4 Beschreibung Das doppelt-logarithmische Diagramm stellt die Zahl der Anzahl der Matrixmultiplikationen pro Sekunde der Matrixgröße gegenüber. Die Größe der Matrix hat qualitativ wenig Einfluss, was zählt, ist der relative Unterschied zwischen den Verfahren. Das Testsystem benutzt eine relativ moderne Quad-Core-CPU. Im Diagramm finden sich vier Kurven: die unterste (und langsamste) entspricht den Perl-Pseudo-Threads, die auf allen Cores laufen dürfen und dies auch weitestgehend tun (die CPU-Auslastung betrug ca. 80%). Darüber befindet sich das gleich Programm, allerdings diesmal auf einen einzelnen Core beschränkt (was im Prinzip Unsinn ist, da immer noch die gleiche Anzahl Threads arbeitet). Darüber befinden sich zwei Kurven, die Coro-Threads benutzen. Die obere, langsamere wurde auf einen Perl mit Pseudo-Thread-Support erzeugt, die darunterliegende auf einem Perl ohne, das Programm selbst war identisch. Da Coro-Threads nicht parallel arbeiten, liefen beide auf einem einzigen Core. Was sofort auffällt, ist der starke Unterschied zwischen echter Parallelverarbeitung und kooperativen Coro-Threads: Die Version mit echten Threads ist am rechten Rand mehr als I<326> mal I als die Version die nur eine CPU benutzt. Weniger auffällig, aber umso interessanter ist es, daß das Perl-Thread-Programm auf einer einzelnen CPU fast 20 mal schneller arbeitet als auf 4 Cores, obwohl beide Pseudothread-Varianten identisch sind. Der Unterschied der Coro-Varianten ist in diesem Diagramm nicht sehr deutlich, beträgt immerhin noch zwischen 25% und 35%, im Vergleich zu den Pseudothread-Varianten verschwindend gering (dies ist ein AMD64-Perl, ein x86-Perl hat weitaus höhere Overheads). =head4 Diskussion Der Benchmark wurde absichtlich so gewählt, daß die Kommunikation einen entscheidenden Bestandteil bildet. Die Pseudo-Threads von Perl sind hierbei doppelt im Nachteil: einerseits besitzen sie keinen gemeinsamen Adressraum (d.h. Variablen müssen kopiert werden), anderseits ist es durch Threads implementiert, und hat dadurch einen erhöhten Aufwand bei der Verwaltung (doppelte Indirektion). Dies allein kann den gewaltigen Unterschied von 31595% (!) erklären. Der Unterschied zwischen den beiden Pseudothreaded-Kurven ist schwerer zu erklären: Da die Programme identisch sind, muss der Unterschied durch andere Effekte erklärt werden: Die Kommunikation läuft in einem Fall weitestgehend über den CPU-Cache, während dieser bei mehrere Cores nicht benutzt werden kann, hier also die Kommunikation über den L2-Cache (ca. 10 mal langsamer) oder den Hauptspeicher (ca. 50 mal langsamer) abläuft. Hinzu kommt, daß während der Kommunikation gemeinsam genutzte Variablen gegen Zugriffe anderer CPUs geschützt werden müssen. Versuchen mehrere Threads gleichzeitig auf diese zuzugreifen, müssen sie sich synchronisieren. Auch dies läuft über den Hauptspeicher ab und ist noch dazu wesentlich aufwändiger. Der Unterschied von ca. 30% bei den beiden Coro-Varianten erklärt sich alleine dadurch, daß ein Pseudo-Threaded-Perl wesentlich mehr Aufwand treiben muss, um die Adressräume der Threads künstlich auseinanderzuhalten, mithin dupliziert es die Arbeit der MMU die in der CPU eingebaut ist und, obwohl Perl effizienter ist, ist eine Software-MMU natürlich immer noch langsamer als eine gut optimierte Hardwarelösung. =head2 Die Herausforderung: Threads in dynamischen Sprachen Thread-Programmierung ist komplex, vor allem, wenn die verwendeten Threads preemptiv sind oder sogar echt parallel arbeiten. Ein gutes Beispiel ist das "double-checked-locking"-Pattern, daß zehn Jahre in jedem Fachbuch stand, bis man feststellte, daß es nicht stabil arbeitet. Für "hardwarenahe" Sprachen ist es sehr viel einfacher, mit Threads umzugehen: viele Datentypen (z.B. C in C) erlauben atomare Zugriffe, Locking ist explizit, der Zugriff selbst (nur lesend oder verändernd) ist genau festgelegt und so weiter. Und wenn's Crasht dann Crasht's eben - in C ist das nichts ungewöhnliches. Für Sprachen wie Perl oder Python stellt sich das ganze anders dar: Selbst "einfache" Integer-Variablen sind auf C-Ebene durch viele Variablen repräsentiert. Dadurch gibt es keine atomaren Zugriffe, d.h. auch lesend zugreifen kann man nur indem man ein Lock benutzt. Hinzu kommt, daß viele Zugriffe, die man als Programmierer als "Lesezugriff" ansieht, intern tatsächlich Schreibzugriffe auslöst. Ein Beispiel: $a .= $b; Ganz klar, C<$a> wird verändert. Nicht so offensichtlich ist, daß auch C<$b> verändert werden kann, z.B. wenn C<$b> noch nicht als String vorliegt. Da dies interne Details des Interpreters betrifft, ist es für einen Perl- (Python-, Ruby- ...) Programmierer nicht ersichtlich, wann man ein Lock/Mutex benutzen muss und wann nicht. Hinzu kommt, daß Programmierer von dynamischen Sprachen einen gewissen Schutz erwarten: Eine Exception zur Laufzeit ist akzeptabel, ein Crash oder gar Memory-Corruption ist es nicht. =head3 Lösungen: Perl Threads wurden zum ersten (und letzten) Mal in Perl 5.005 eingeführt. Das 5.005-Modell implementierte echte Threads, die, sofern die Platform Kernel-Threads unterstützte, auch parallel liefen. Crashes in diesem Modell waren die Tagesordnung. Offenbar war es Perl-Programmierern nicht zu vermitteln, daß man in zwei Threads nicht gleichzeitig (auch nicht lesend) auf dieselbe Variable zugreifen darf. Dies führte dazu, daß dieses Modell in Verruf geriet, obwohl es eigentlich nur das lieferte, was Threads bedeuten: schwierige Programmierung, Crashes und so weiter. Tatsächlich zeigt dies ein generelles Problem bei Threads auf: Sie lassen sich nicht abstrahieren: Stabile Programme erhält man unter anderem durch Abstraktion und Schichtung: einzelne Aufgaben werden in Komponenten abstrahiert (z.B. Module), die die Details regeln, während der Benutzer dieser Komponenten sich nicht um die Details kümmern braucht. Bei Sicherheit funktioniert dies nicht gut: TLS z.B. ist sehr sicher, dennoch darf man (wie Microsoft schon häufig bewiesen hat) nicht naiv davon ausgehen, daß eine Verbindung abhörsicher oder privat wäre, nur, weil man TLS benutzt. Ähnlich ist es mit Threads: sicherlich kann man Queues und ähnliches abstrahieren, aber in vielen Fällen muss man dennoch wissen, wie diese Komponenten implementiert sind, um z.B. Deadlocks zu vermeiden. Und wenn man Daten an andere Threads sendet, muss man sowieso immer selbst den Zugriff regeln, oder selbst Kopien herstellen. Dies ist grundsätzlich unvereinbar mit dem Anspruch, Threads in Hochsprachen sicher gestalten zu können, es sei denn, man belegt alles mit einem Lock. In der Praxis wird die Implementation dadurch aber sehr viel langsamer, was den Vorteil durch Parallelverarbeitung mehr als aufwiegt. Das 5.005-Modell verschwand also wieder in der Versenkung, dennoch wollte man irgendwelche "Threads" anbieten. =head4 Windows - Wurzel allen Übels Und die "Rettung" war Windows. Genaugenommen die Ineffizienz von Windows. Dazu muss ich etwas ausholen. Unix (und Perl) haben eine sehr elegante Methode, neue Prozesse zu erzeugen, die man fork+exec nennt. In C (und so ähnlich auch in Perl) sieht das z.B. so aus (ohne Fehlerbehandlung): if (fork () > 0) execl ("/bin/echo", "echo", "Hello, World", (char *)0); Anders als Windows wird das Erzeugen eines neuen Prozesses in zwei Teile zerlegt: zuerst wird eine Kopie des Prozesses erzeugt (fork), dann wird die Kopie durch einen anderen Prozess ersetzt (exec). Das Erstellen einer Kopie ist dank direkter Unterstützung durch die CPU sehr schnell, dennoch mutet es zuerst merkwürdig an. Der Grund, weshalb dies so elegant ist, liegt darin, daß man die Umgebung des neuen Prozesses nicht nur sehr flexibel gestalten kann, sondern auch genau die gleichen Funktionen benutzen kann, die man auch sonst einsetzt. Hier z.B. wird STDIN/STDOUT/SDTERR nach F umgeleitet: if (fork () > 0) { int fd = open ("/dev/null", O_RDWR); dup2 (fd, 0); dup2 (fd, 1); dup2 (fd, 2); execl ("/bin/echo", "echo", "Hello, World", (char *)0); } Obwohl heutige CPUs dieses Verfahren gut unterstützen, erlaubt Microsoft Windows dies nicht. Stattdessen muss man sehr umständliche funktionen mit zig Parametern aufrufen: CreateProcess (NULL, // No module name (use command line). cmdline, // Command line. NULL, // Process handle not inheritable. NULL, // Thread handle not inheritable. FALSE, // Set handle inheritance to FALSE. 0, // No creation flags. NULL, // Use parent's environment block. NULL, // Use parent's starting directory. &si, // Pointer to STARTUPINFO structure. &pi) // Pointer to PROCESS_INFORMATION structure. Tatsächlich sind viele dieser Parameter Zeiger auf Strukturen mit sehr vielen weiteren Parametern. Dies liegt daran, daß Windows praktisch jeden möglichen Konfigurationswunsch im Voraus anbieten muss, da man nicht einfach zwischen Prozesserzeugung und Programmstart irgendwelche Funktionen aufrufen kann. (Irgendwie wird einem dabei auch klar, wieso Windows-Programme von so schlechter Qualität sind, wer soll ich den ganzen Misst auch merken können). Nun gut, die native-Win32-Perls standen also vor dem Problem, daß das elegante fork+exec bei Programmierern sehr beliebt war, ihre Platform dies aber nicht unterstützte. Statt wie bei Cygwin eine saubere (und grausam langsame) fork-Emulation zu implementieren, griff man allerdings bei ActiveState zu einem grausamen (und noch langsamerem) Hack: Bei jedem fork wird der komplette Perl-Interpreter mit I Variablen kopiert. Damit Perl nicht durcheinander kommt, wird I Funktion ein zusätzlicher Parameter übergeben, der angibt, wo man die globalen (bzw. gerade nicht mehr globalen) Variablen findet. Dies bedeutet erheblichen zusätzlichen Aufwand, weshalb Perl sehr viel langsamer wird, natürlich auch dann, wenn man keine Threads benutzt. Hinzu kommt, daß das Kopieren sehr komplex und fehlerbehaftet ist (Crashes sind an der Tagesordnung) und natürlich noch langsamer als blind den gesamten Prozess zu kopieren (wie es Cygwin macht). Das schlimmste jedoch ist, daß diese Fork-Emulation auf einen Schlag praktisch alle XS-Module kaputt macht - bis heute funktionieren die meisten XS-Module deshalb nicht sauber unter Windows. Tragisch ist, daß diese fork-Emulation bis heute nicht richtig funktioniert - Memory Leaks und Crashes sind das Ergebnis. Für Windows wahrhaft angemessen, wahrscheinlich fällt Windows-Programmierern dies gar nicht besonders auf. Solange dies auf Windows beschränkt bleibt, ist das natürlich relativ egal. Leider hatte jemand eine geniale Idee: =head4 Wenn wir Threads benutzen um Prozesse zu emulieren... ... dann behaupten wir einfach, unsere Prozesse sind Threads und wir haben Threads. Ich kenne die genauen Details nicht, aber irgendein I fand es cool, alle Perl-Programmierer zu verarschen, Perl überall sehr viel langsamer zu machen und das ganze dann Threads zu nennen: Die Interpreter-Threads waren geboren. Genausowenig, wie Perl kein C ist, obwohl es mit C implementiert ist, sind die Perl-Threads keine Threads, nur weil sie mit Threads implementiert werden. Die sogenannten Perl-Threads sind nichts anderes, als die Windows-Prozess-Emulation auf Unix portiert, mit allen Nachteilen: Nicht nur besitzen die Perl-Pseudo-Threads keinen gemeinsamen Adressraum, sie machen den Perl-Interpreter langsamer und liefern nichts, was jedes Unix nicht wesentlich effizienter mit Hardwareunterstützung erledigen kann. Alle weiteren Effizienzprobleme die durch Kernel-Threads entstehen, sind natürlich auch vertreten. Ohne jeglichen Vorteil aus dem ganzen zu ziehen. Oder doch? Ein genialer Schachzug eigentlich: Man schädigt Perl auf jeder Plattform, auch denjenigen, die dies gar nicht nötig haben, und erhält gleichzeitig ein Buzzword "Thread-Support". Dazu gehört schon Chutzpe. Dies sind die Gründe, weshalb die Perl-Pseudo-Threads sterben sollten: sie verkomplizieren XS-Module unnötig; sind extrem langsam, selbst, wenn man sie gar nicht benutzt; sie funktionieren auf keiner Plattform richtig und vor allem: es sind keine Threads! Wie machen's dann die Anderen? =head3 Python, Ruby und die Anderen Andere Sprachen, die in einer ähnlichen Liga wie Perl spielen, leiden natürlich unter denselben Problemen. Ihre Lösung wird gerne belächelt - Kooperative Threads. Richtig, bei Python läuft nichts parallel. Zumindest sehr wenig. Z.B. I/O. Oder Datenbankzugriffe. Und das sind eigentlich alles die Dinge, die man gerne parallel laufen lassen möchte.... Ruby and die meisten anderen dynamischen Sprachen, die Threads implementieren, machen genau dasselbe. Natürlich ist nicht alles perfekt: Python und Ruby benutzen z.B. Kernel-Threads um Userspace-Threads zu emulieren. Dies ist natürlich ineffizient, dafür aber portabel. =head4 Perl kann's auch - Coro Tatsächlich gibt's für Perl auch echte Threads, nämlich das Coro-Modul, das ebenfalls kooperative Threads implementiert. Anders als Python unterstützt es auch schnelle Userspace-Threads (auf den Plattformen, die nicht völlig kaputt sind, d.h. alles außer den BSDs, wo man aufgrund von libc-Bugs langsame OS-Threads benutzen muss). Tatsächlich gibt es Pläne, eine Art Hybrid-Modell zu implementieren: Auf Perl-Ebene hätte man das Python-Thread-Modell, auf C-Ebene kann man sich aber eben mal einen OS-Thread "borgen", um z.B. asynchrones I/O oder asynchrone Datenbank-Zugriffe zu realisieren, wofür aber support der jeweiligen Module notwendig wird. =head3 Erlang Erlang hat auf den ersten Blick noch ein ganz anderes Thread-Modell, nämlich das "echte": "Variablen" sind "geshared" und Threads laufen parallel. Allerdings nur auf den ersten Blick: Erlang besitzt gar keine "Variablen", also sind alle Zugriffe erstmal nur Lesend. Außerdem sind die Erlang-Entwickler Profis, was Ausfallsicherheit angeht, und das erreicht man nun mal nur mit Redundanz. Redundanz bedeutet aber auch Abstand, d.h. Erlang-"Threads" müssen auf völlig getrennten Rechnern laufen können. Ergo implementiert Erlang schnelle Userspace-Threads und benutzt für echte Parallelität Prozesse mit nur teilweise gemeinsamen Adressraum, und hat damit alle Vorteile, die man durch Parallelität erreichen kann. Natürlich könnte man die Sprache selbst als gravierenden Nachteil betrachten... Es ist halt nicht Perl. =head2 Schlußwort Zum Wohl aller: Perl-"Threads" müssen sterben. Die Perl-Threads sollen leben!