| 1 |
root |
1.1 |
=encoding utf-8 |
| 2 |
|
|
|
| 3 |
|
|
=head1 Threads in dynamischen Sprachen und warum Perl-Pseudo-Threads sterben sollten |
| 4 |
|
|
|
| 5 |
|
|
Wie man am Titel sieht, soll es im folgenden um zwei verwandte Themen |
| 6 |
|
|
gehen. Einerseits möchte ich grundsätzliche Gedanken über Threads |
| 7 |
|
|
liefern, einige Vorurteile ausräumen und erläutern, wie Threads in |
| 8 |
|
|
dynamischen Sprachen implementiert werden und warum dies so ist. |
| 9 |
|
|
|
| 10 |
|
|
Andererseits möchte ich erläutern, warum Perl keine Threads unterstützt |
| 11 |
|
|
und weshalb Perls sogenannte Threads eher sterben sollten, da sie |
| 12 |
|
|
mehr Schaden anrichten als sie helfen. Letzteres muss nicht einmal zu |
| 13 |
|
|
Inkompatibilitäten führen: da Perl-Threads keine sind, lassen sie sich |
| 14 |
|
|
effizient ohne emulieren. |
| 15 |
|
|
|
| 16 |
|
|
=head2 Was ist ein Thread - Eine Begriffsbestimmung |
| 17 |
|
|
|
| 18 |
|
|
Am einfachsten erklärt man Threads, in dem man sie Prozessen |
| 19 |
|
|
gegenüberstellt: |
| 20 |
|
|
|
| 21 |
|
|
Prozesse haben (üblicherweise) einen eigenen Adressraum, d.h. Variablen |
| 22 |
|
|
existieren als Kopie in jedem Prozess und sind unabhängig |
| 23 |
|
|
voneinander. Außerdem besitzt ein Prozess eine "Continuation", d.h. im |
| 24 |
|
|
wesentlichen den aktuellen "Ort" der Programmausführung. Unter Unix |
| 25 |
|
|
kommen dann noch andere Ressourcen wie File-Descriptoren hinzu, aber im |
| 26 |
|
|
wesentlichen ist es das. |
| 27 |
|
|
|
| 28 |
|
|
Ein Thread nun ist im wesentlichen ein abgespeckter Prozess (daher werden |
| 29 |
|
|
sie auch oft "LWPs" - Light Weight Processes genannt), bei dem eine oder |
| 30 |
|
|
mehrere dieser Ressourcen gemeinsam genutzt werden, insbesondere der |
| 31 |
|
|
Adressraum. Daher kann man Threads auch als Untereinheit von Prozessen |
| 32 |
|
|
ansehen: ein Prozess kann aus ein oder mehreren Threads bestehen, die sich |
| 33 |
|
|
alle den gleichen Adressraum (die gleichen Variableninhalte) teilen. |
| 34 |
|
|
|
| 35 |
|
|
Bei Threads gibt eine Menge verschiedene Variationen. Die wichtigsten sind: |
| 36 |
|
|
|
| 37 |
|
|
=over 4 |
| 38 |
|
|
|
| 39 |
|
|
=item Kooperative Threads |
| 40 |
|
|
|
| 41 |
|
|
Die "Urform" der Threads - diese Threads werden "kooperativ" genannt, |
| 42 |
|
|
nicht, weil sie es sind, sondern, weil sie es sein müssen: Sobald |
| 43 |
|
|
ein Thread ausgeführt wird, läuft dieser ohne Unterbrechung, bis er |
| 44 |
|
|
freiwillig die CPU an einen anderen Thread abgibt. Tut ein Thread dies |
| 45 |
|
|
nicht, läuft nichts anderes im System. |
| 46 |
|
|
|
| 47 |
|
|
Manchmal werden Kooperative Threads auch Koroutinen, Fibers oder |
| 48 |
|
|
Continuations genannt, was genau genommen falsch ist, aber im wesentlichen |
| 49 |
|
|
bezeichnen diese Begriffe dasselbe. |
| 50 |
|
|
|
| 51 |
root |
1.2 |
Die Hauptvorteile von kooperativen Threads ist die vergleichsweise |
| 52 |
|
|
einfache Programmierung (Race-conditions sind wesentlich einfacher zu |
| 53 |
|
|
vermeiden) und ihre hohe Effizienz. Hauptnachteil ist, daß ein einzelner |
| 54 |
|
|
wildgewordener Thread das gesamte System lahmlegen kann. |
| 55 |
|
|
|
| 56 |
root |
1.1 |
Windows 3.11, das Oberon-System oder das Coro-Modul von Perl sind |
| 57 |
|
|
Beispiele für kooperatives Threading. |
| 58 |
|
|
|
| 59 |
|
|
=item Preemptive Threads |
| 60 |
|
|
|
| 61 |
|
|
Um das Problem der Monopolisierung des Rechners durch einen unkooperativen |
| 62 |
|
|
(weil z.B. ferhlerhaften) Thread zu umgehen, wurden "preemptive" Threads |
| 63 |
|
|
erfunden: Diese werden ohne ihr Zutun regelmäßig "unterbrochen" (z.B. |
| 64 |
|
|
durch einen Timer) und dadurch von der CPU "verdrängt" (preempted). |
| 65 |
|
|
|
| 66 |
|
|
Diese Art von Threads wird oft auch "time-slicing" (Zeitscheibenverfahren) |
| 67 |
|
|
genannt, da hierbei ein Thread üblicherweise eine bestimmte Zahl von |
| 68 |
|
|
Zeiteinheiten erhält und, wenn er in dieser Zeit die CPU nicht freigibt, |
| 69 |
|
|
wird er automatisch verdrängt. |
| 70 |
|
|
|
| 71 |
root |
1.2 |
Hauptvorteil von preemptiven Systemen ist, daß man wildgewordene |
| 72 |
|
|
Programme abbrechen kann, d.h. sie legen nicht das gesamte System |
| 73 |
|
|
lahm. Hauptnachteil ist, daß ihre Benutzung sehr kompliziert ist: auch |
| 74 |
|
|
Experten machen regelmäßig Fehler und erzeugen Race-Conditions. |
| 75 |
|
|
|
| 76 |
root |
1.1 |
Neuere Windows-Versionen, alle Unix-Versionen und insbesondere alle |
| 77 |
|
|
Thread-Systeme dynamischer Sprachen (d.h. Ruby, Python usf.) benutzen |
| 78 |
|
|
dieses Verfahren. |
| 79 |
|
|
|
| 80 |
|
|
=item "Kernel-Threads" und "Userspace-Threads" |
| 81 |
|
|
|
| 82 |
|
|
Ein weiterer wichtiger Unterschied zwischen Thread-Systemen ist, ob sie |
| 83 |
|
|
vom Betriebssystem selbst implementiert werden oder innerhalb eines |
| 84 |
|
|
Prozesses. |
| 85 |
|
|
|
| 86 |
|
|
In letzterem Fall teilen sich alle Threads dieselbe Prozess-Continuation, |
| 87 |
|
|
was nichts anderes bedeutet, als daß diese Threads niemals parallel |
| 88 |
|
|
laufen können (z.B. auf einem SMP-System). |
| 89 |
|
|
|
| 90 |
|
|
Der Begriff "Kernel-Thread" bezeichnet im Gegensatz dazu die Variante, bei |
| 91 |
|
|
er die Threads tatsächlich parallel/gleichzeitig arbeiten können, obwohl |
| 92 |
|
|
dies nicht zwangsweise im "Kernel" implementiert werden muss. |
| 93 |
|
|
|
| 94 |
|
|
Userspace-Threads sind im allgemeinen um ein vielfaches schneller als |
| 95 |
|
|
Kernel-Threads - die Userspace-Threading-Bibliothek, die von Coro benutzt |
| 96 |
|
|
wird schaltet mehr als 100 mal schneller zwischen Threads um als der |
| 97 |
|
|
Linux-Kernel. Selbst auf Perl-Ebene schalten die Userspace-Threads von |
| 98 |
|
|
Coro mehr als 14 mal schneller als die Linux-Kernel-Threads. |
| 99 |
|
|
|
| 100 |
root |
1.2 |
Kernel-Threads verschärfen gegenüber Userspace-Threads die Problematik |
| 101 |
|
|
der Programmierung nochmals. Ein gutes Beispiel ist MythTV, ein |
| 102 |
|
|
C++-Programm, daß Threads benutzt: auf Single-Core-Systemen arbeitet |
| 103 |
|
|
es meist korrekt, auf Multi-Core-Systemen sind crashes aber an der |
| 104 |
|
|
Tagesordnung, da der Programmierer offenbar nicht damit rechnet, |
| 105 |
|
|
daß Speicherzugriffe nicht mehr atomar sind bzw. die Gefahr einer |
| 106 |
|
|
Race-Condition stark erhöht ist. |
| 107 |
|
|
|
| 108 |
root |
1.1 |
=back |
| 109 |
|
|
|
| 110 |
|
|
=head2 Wozu wurden Threads erfunden, wozu eignen sie sich? |
| 111 |
|
|
|
| 112 |
|
|
=head3 Single-Cores |
| 113 |
|
|
|
| 114 |
|
|
Multi-Core und Multiprozessorsysteme sind erst seit kurzer Zeit |
| 115 |
|
|
verbreitet. Die überwiegende Mehrheit der Rechner besitzt nach wie vor |
| 116 |
|
|
nur einzelne CPUs. |
| 117 |
|
|
|
| 118 |
|
|
Moderne CPUs besitzen im allgemeinen spezielle Hardware, um Prozesse zu |
| 119 |
|
|
unterstützen, z.B. eine Memory-Management-Unit (MMU) um den Adressraum |
| 120 |
|
|
zu verwalten und einen Translation-Lookaside-Buffer (TLB) um das ganze zu |
| 121 |
|
|
beschleunigen. |
| 122 |
|
|
|
| 123 |
|
|
Da jeder Prozess seinen eigenen Adressraum besitzt, muss die MMU bei jedem |
| 124 |
|
|
Umschalten neu konfiguriert werden, Caches wie der TLB gehen häufig |
| 125 |
|
|
verloren. Dies kann die Performance empfindlich beeinflussen, da die meisten |
| 126 |
|
|
CPUs nur einen Zustand im Cache behalten können. |
| 127 |
|
|
|
| 128 |
|
|
Die natürliche Lösung gegen zu viele Rekonfigurationen sind weniger |
| 129 |
|
|
Rekonfigurationen bei Prozesswechseln. Wechselt man den Adressraum nicht, |
| 130 |
|
|
so erhält man Threads auf natürliche Weise, da die CPU einfach den alten |
| 131 |
|
|
Adressraum weiter benutzt. |
| 132 |
|
|
|
| 133 |
|
|
Daher stammt der Name "Light Weight Processes" (leichtgewichtige |
| 134 |
|
|
Prozesse): Threads werden hier als Prozesse mit weniger "Zustand" behandelt. |
| 135 |
|
|
|
| 136 |
|
|
Dies beschleunigt das Prozessumschalten erheblich, vor allem auf älteren |
| 137 |
|
|
Systemen. |
| 138 |
|
|
|
| 139 |
|
|
=head3 Multiprozessor- oder Multi-Core-Systeme |
| 140 |
|
|
|
| 141 |
|
|
Ein weit verbreiteter Irrtum ist es, daß Threads sich gut für die |
| 142 |
|
|
Parallelisierung eignen, d.h. echte Parallelverarbeitung auf einem |
| 143 |
|
|
Multiprozessorsystem. |
| 144 |
|
|
|
| 145 |
|
|
Dies ist allerdings nicht der Fall: Das Problem, daß es nur eine Instanz |
| 146 |
|
|
der Caches oder MMU gibt, existiert bei mehreren CPU-Kernen nicht, da |
| 147 |
|
|
diese alle ihren eigenen Cache besitzen. |
| 148 |
|
|
|
| 149 |
|
|
Im Gegenteil: Dadurch, daß Threads viele Ressourcen gemeinsam nutzen, |
| 150 |
|
|
muss das Betriebssystem durch aufwändige (und langsame) Verwaltung |
| 151 |
|
|
sicherstellen, daß auch alle CPUs im System den gleichen Zustand haben. |
| 152 |
|
|
|
| 153 |
|
|
Wenn z.B. ein Prozess mit mehreren Threads auf einem Multi-Core-System |
| 154 |
root |
1.2 |
arbeitet und der eine Thread vom Betriebssystem Speicher anfordert, |
| 155 |
|
|
so muss das Betriebssystem synchron alle anderen CPUs anhalten, ihnen |
| 156 |
|
|
den neuen Zustand mitteilen, warten, bis alle den Zustand übernommen |
| 157 |
|
|
haben und kann erst dann weiterarbeiten. Auf einem Dual-Core-System mag |
| 158 |
|
|
dies noch akzeptabel sein, bei mehr Cores wird dies aber schnell sehr |
| 159 |
|
|
aufwändig, auf Mehrfachprozessorsystemen, bei denen die Kommunikation |
| 160 |
|
|
zwischen CPUs noch länger braucht, wird dies schnell inakzeptabel. |
| 161 |
|
|
|
| 162 |
|
|
Aber auch der ganze normale Speicher-Cache wird nicht effizient |
| 163 |
|
|
genutzt: Da Threads alle Variablen gemeinsam nutzen, liegen gemeinsam |
| 164 |
|
|
genutzte Variablen nicht im lokalen Cache einer CPU, im Gegenteil, sie |
| 165 |
|
|
wandern häufig zwischen den Caches hin- und her, d.h. die Kommunikation |
| 166 |
|
|
läuft über den (vergleichsweise langsamen) Hauptspeicher ab. |
| 167 |
|
|
|
| 168 |
|
|
Letzteres wird häufig dadurch verhindert, daß man unterschiedliche |
| 169 |
|
|
Speicherbereiche in jedem Thread verwendet. Allerdings wird dies durhc |
| 170 |
|
|
eine zusätzliche Indirektion bei jedem Zugriff bezahlt. Perls, die diese |
| 171 |
|
|
Methode benutzten, werden dadurch zwischen 15 und ca. 200% langsamer als |
| 172 |
|
|
Perls, die dies nicht tun (auch ohne Parallelbetrieb). |
| 173 |
|
|
|
| 174 |
|
|
Ein Vorteil von Threads ist die vergleichsweise schnelle Kommunikation |
| 175 |
|
|
über den Speicher, was bei Programmen, die viel Kommunikation betreiben, |
| 176 |
|
|
einen großen Gewinn bedeutet, wenn die alternative z.B. Kommunikation über Sockets ist. |
| 177 |
|
|
|
| 178 |
|
|
Moderne Betriebssysteme erlauben es aber ausnahmslos, einzelne |
| 179 |
|
|
Speicherbereiche gemeinsam zu nutzen, so daß Prozesse mit selektiv |
| 180 |
|
|
gemeinsam genutzten Speicher wesentlich effizienter auf Mehr-CPU-Systemen |
| 181 |
|
|
arbeiten, als Threads. |
| 182 |
|
|
|
| 183 |
|
|
Am folgenden "Benchmark" wird dies deutlich. Das Benchmark-Programm |
| 184 |
|
|
implementiert eine parallel Matrixmultiplikation, bei der einzelne |
| 185 |
|
|
Zeilen und Spalten an vier "Arbeits-Threads" gesendet werden, die eine |
| 186 |
|
|
Skalarmultiplikation durchführen und an einen Ergebnis-Thread senden. |
| 187 |
|
|
|
| 188 |
|
|
Dabei kann es Perl-Pseudo-Threads (die parallel arbeiten) und Coro-Threads |
| 189 |
|
|
benutzen. |
| 190 |
root |
1.1 |
|
| 191 |
root |
1.2 |
Das Programm findet sich unter F<http://data.plan9.de/matmult>, ist aber |
| 192 |
|
|
für die Diskussion nicht relevant. |
| 193 |
root |
1.1 |
|
| 194 |
|
|
=begin latex |
| 195 |
|
|
|
| 196 |
|
|
\begin{center} |
| 197 |
|
|
\includegraphics{mat2.eps} |
| 198 |
|
|
\end{center} |
| 199 |
|
|
|
| 200 |
|
|
=end latex |
| 201 |
|
|
|
| 202 |
root |
1.2 |
Das doppelt-logarithmische Diagramm stellt die Zahl der Anzahl der |
| 203 |
|
|
Matrixmultiplikationen pro Sekunde der Matrixgröße gegenüber. Die |
| 204 |
|
|
Größe der Matrix hat qualitativ wenig Einfluss, was zählt, ist der |
| 205 |
|
|
relative Unterschied zwischen den Verfahren. Das Testsystem benutzt eine |
| 206 |
|
|
relativ moderne Quad-Core-CPU. |
| 207 |
|
|
|
| 208 |
|
|
Im Diagramm finden sich vier Kurven: die unterste (und langsamste) |
| 209 |
|
|
entspricht den Perl-Pseudo-Threads, die auf allen Cores laufen dürfen und |
| 210 |
|
|
dies auch weitestgehend tun (die CPU-Auslastung betrug ca. 80%). Darüber |
| 211 |
|
|
befindet sich das gleich Programm, allerdings diesmal auf einen einzelnen |
| 212 |
|
|
Core beschränkt (was im Prinzip Unsinn ist, da immer noch die gleiche |
| 213 |
|
|
Anzahl Threads arbeitet). |
| 214 |
|
|
|
| 215 |
|
|
Darüber befinden sich zwei Kurven, die Coro-Threads benutzen. Die obere, |
| 216 |
|
|
langsamere wurde auf einen Perl mit Pseudo-Thread-Support erzeugt, |
| 217 |
|
|
die darunterliegende auf einem Perl ohne, das Programm selbst war |
| 218 |
|
|
identisch. Da Coro-Threads nicht parallel arbeiten, liefen beide auf einem |
| 219 |
|
|
einzigen Core. |
| 220 |
|
|
|
| 221 |
|
|
Was sofort auffällt, ist der starke Unterschied zwischen echter |
| 222 |
|
|
Parallelverarbeitung und kooperativen Coro-Threads: Die Version mit echten Threads |
| 223 |
|
|
ist am rechten Rand mehr als I<326> mal I<langsamer> |
| 224 |
|
|
|
| 225 |
|
|
|
| 226 |
|
|
|
| 227 |
|
|
|
| 228 |
|
|
|
| 229 |
|
|
|
| 230 |
|
|
|
| 231 |
|
|
|
| 232 |
|
|
|
| 233 |
|
|
|