ViewVC Help
View File | Revision Log | Show Annotations | Download File
/cvs/docs/pws2009/threads.pod
Revision: 1.2
Committed: Sat Jan 17 03:37:42 2009 UTC (17 years, 8 months ago) by root
Branch: MAIN
Changes since 1.1: +88 -5 lines
Log Message:
*** empty log message ***

File Contents

# User Rev Content
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