Posted on 1 Comment

Multithreading

Multithreading

Multithreading (auch Nebenläufigkeit oder Mehrsträngigkeit) bezeichnet das parallele Abarbeiten mehrerer Ausführungsstränge, sogenannter Threads, innerhalb eines Prozesses. Dadurch kann ein System schneller auf Ereignisse reagieren und die zur Verfügung stehende Hardware besser ausnutzen. Insbesondere durch die wachsenden Anforderungen an Software und immer leistungsstärkere Hardware gewinnt Multithreading in der modernen Softwareentwicklung an Bedeutung. Dementsprechend gibt es mittlerweile einige unterschiedliche Möglichkeiten, wie Multithreading in der Praxis umgesetzt werden kann. 

Begriffliche Basics

Um Multithreading, seine Implikationen und Möglichkeiten der Umsetzung zu verstehen, gibt es einige Begriffe, die zuerst geklärt werden müssen. Dazu gehören unter anderem ProzessThreadKontext oder Kontextwechsel. Ein generelles Grundwissen darüber, wie ein Betriebssystem arbeitet oder die Hardware eines Computers aufgebaut ist, hilft ebenfalls dabei, Multithreading richtig zu begreifen und umzusetzen. 

Prozess

Ein Prozess (auch als Task bezeichnet) ist ein Computerprogramm zur Laufzeit bzw. die Ablaufumgebung des Programmes. Ein Prozess umfasst zum einen die Instanz des Programmes, aber auch Verwaltungsinformationen oder Ressourcen wie Speicherplatz und Speicheradressen. Wird ein Prozess zum aktuellen Zeitpunkt auf der CPU ausgeführt, wird er als running bezeichnet. Wird der Prozess gestoppt, geht er in den Zustand ready. Von diesem Zustand kann er, sobald ihm Ressourcen zur Verfügung gestellt werden, seine Ausführung fortsetzen und in den Zustand running zurückkehren. Wenn der Prozess auf ein anderes Ereignis warten muss und dadurch auch mit Ressourcen oder CPU nicht weiterarbeiten kann, ist er blocked. Zuletzt gibt es den Zustand terminated, der erreicht wird, wenn der Prozess seine Ausführung beendet hat oder beendet wurde. Die einzelnen aktiven Prozesse (also die Prozesse im Zustand readyrunning und blocked) werden durch das Betriebssystem mittels eines Schedulers verwaltet. Dieser kann zwischen einzelnen Prozessen wechseln.

Thread

Ein Thread wird auch als Aktivitätsträger oder leichtgewichtiger Prozess bezeichnet. Es handelt sich um einen Ausführungsstrang, der zu der Ausführung eines Programms, also einem Prozess, gehört. Jeder Prozess besitzt zumindest einen Thread.

(Prozess-)Kontext

Einer der wichtigsten Begriffe, den man im Zusammenhang mit Multithreading verstehen muss, ist der Prozesskontext. Der Prozesskontext bezeichnet die gesamte Information, die vom Betriebssystem bzw. Prozessor zur Verwaltung eines Prozesses benötigt wird. Dazu gehören der Benutzerkontext (alle Daten des Prozesses in seinem Adressraum), der Hardwarekontext (u.a. Basisregister, Grenzregister oder Befehlszählregister) und der Systemkontext (die Informationen direkt zum Prozess, bspw. seine Nummer, seine geöffneten Dateien oder seine Eltern- und Kindprozesse). Die eben erwähnten Register sind Speicherbereiche für Daten, auf die das Betriebssystem schnell zugreifen kann. 

Kontextwechsel

Wenn das Betriebssystem zwischen zwei Prozessen oder zwei Threads wechselt, wird dies als Kontextwechsel(context switch) bezeichnet. Für einen Prozess bedeutet dies, dass das System den aktuellen Prozess so abspeichern muss, dass er ihn zu einem beliebigen Zeitpunkt von genau der aktuellen Stelle fortsetzen kann, und anschließend den anderen Prozess lädt. Das kann beispielsweise nötig sein, um Speicherplatz freizugeben oder auf ein Event – wie Tastatur- oder Mauseingaben – zu reagieren. Bei einem reinen Thread-Kontextwechsel (thread switch) hingegen muss der virtuelle Speicher nicht neu geladen werden, insofern beide Threads im gleichen Prozess liegen. 

Grundlagen von Multithreading

Klassische Software arbeitet sequentiell – das heißt, der Code wird Zeile für Zeile synchron abgearbeitet. Vor jedem nächsten Schritt muss der aktuelle Schritt abgeschlossen werden. Das funktioniert für viele Anwendungsfälle, ist allerdings ineffizient. So kann die Software nicht schnell auf Ereignisse reagieren und langwierige Rechenprozesse blockieren die Hardware. Multithreading ermöglicht es, mehrere Ausführungsstränge innerhalb eines Prozesses gleichzeitig auszuführen. Dadurch werden Programme zum einen reaktiver und zum anderen nutzen sie die zugrundeliegende Hardware besser aus und werden so in ihrer Gesamtleistung schneller. Allerdings ist die Implementierung von Software mit Multithreading meist komplexer als einfache sequentielle Programme. Auch die Fehlersuche gestaltet sich in Programmen mit Multithreading schwieriger. Ein besonders wichtiger Faktor dabei sind sogenannte Race Conditions – Anwendungsfehler, bei denen das Ergebnis einer Operation von der zeitlichen Abfolge der Teiloperationen abhängt. Um solche Race Conditions zu vermeiden, müssen einzelne Threads innerhalb des Prozesses synchronisiert werden. Die Aufgabe, die Software bereits korrekt in Threads aufzuteilen und dafür zu sorgen, dass das Betriebssystem bei der Ausführung keine ungewollten Nebeneffekte erzeugt, obliegt dem Entwickler. 

Man unterscheidet bei Multithreading zudem zwischen echter und Pseudo-Gleichzeitigkeit. Bei echter Gleichzeitigkeit oder Parallelität können tatsächlich mehrere Threads gleichzeitig abgearbeitet werden, beispielsweise weil mehrere Prozessorkerne zur Verfügung stehen. Steht nur ein Prozessorkern zur Verfügung, ist es dem Betriebssystem oder der Software trotzdem möglich durch geschickte Kontextwechsel den Anschein von Gleichzeitigkeit zu erwecken. Durch die Kontextwechsel werden mehrere Prozesse parallel abgearbeitet und sind aktiv, obwohl immer nur ein Prozess tatsächlich im Zustand running ist.

Umsetzung von Multithreading

Es gibt verschiedene Möglichkeiten, wie Multithreading umgesetzt werden kann. Einer der wichtigsten Aspekte zur Unterscheidung ist, ob die Koordination der Threads auf Seite der Hardware oder der Software stattfindet. Die konkrete Umsetzung auf Software- bzw. Hardwareebene kann dann noch einmal differenziert betrachtet werden.

Softwareseitiges Multithreading

Bei softwareseitigem Multithreading geschieht die Aufteilung des Programms in mehrere Threads ausschließlich im Programm selbst – für die Hardware bzw. das Betriebssystem liegt also immer nur ein einzelner Ausführungsstrang vor. Die von außen wahrgenommene Gleichzeitigkeit des Programms wird durch intelligente Sequenzierung bzw. Thread-Priorisierung bewerkstelligt. Ein Thread erhält dabei eine bestimmte Priorität, die bestimmt, wann es in den Hauptspeicher geladen und abgearbeitet wird bzw. CPU zur Verfügung erhält. Dadurch können einzelne Threads des Prozesses schnell auf zeitkritische Events reagieren, während Threads, die rechenintensive oder langwierige Berechnungen durchführen, im Hintergrund laufen und dann fortgesetzt werden, wenn der Prozess nicht mit einer “wichtigeren” Aufgabe beschäftigt ist. 

Ein Beispiel, an dem man sich dieses Prinzip besser vorstellen kann, ist ein Viedo: Eine Szene (Thread 1) wird lediglich im Hintergrund betrachtet und führt beispielsweise eine Berechnung durch, während eine weitere Szene (Thread 2) im Vordergrund betrachtet wird und auf Nutzereingaben reagieren kann. Ist die vordergründige Szene abgeschlossen, kann man sich wieder mit dem Hintergrund beschäftigen, bis eine neue Szene mit höherer Priorität beginnt.

Die Gesamtleistung des Systems hängt bei softwareseitigem Multithreading davon ab, wie unabhängig die einzelnen Threads voneinander sind. Bei Abläufen, die möglichst unabhängig voneinander sein sollen, ist der Kontextwechsel weiterhin recht teuer, sodass es zu Performance-Einbußen kommen kann.

Hardwareseitiges Multithreading

Bei hardwareseitigem Multithreading unterstützt die Hardware eines Prozessors das Abarbeiten mehrerer Prozessstränge entweder gleichzeitig oder pseudo-gleichzeitig. Die Software muss diese Fähigkeit des Prozessors nutzen und mehrere Threads verwenden. Im Gegensatz zum rein softwareseitigem Multithreading sind nun aber auch von außen mehrere Threads sichtbar, sodass das Betriebssystem die Threads aktiv koordinieren kann. Die meisten modernen Betriebssysteme unterstützen Multithreading. Zudem haben die meisten Computer mehrere Prozessorkerne, die eine echte parallele Ausführung der Threads erlauben. Die einzelnen Prozessorkerne arbeiten dabei oft pseudo-gleichzeitig, das heißt, die Threads werden nicht nur auf die Prozessorkerne verteilt, sondern dort auch durch Kontextwechsel parallel bearbeitet. Die beiden wichtigsten Varianten für diese Art von pseudo-gleichzeitigem Multithreading sind Time Slicing und Switch on Event Multithreading (SoEMT).

Time Slicing

Bei Time Slicing erhält jeder Prozess einen durch einen Algorithmus bestimmten Zeitabschnitt, in dem er abgearbeitet wird. Ist die Zeit abgelaufen, wird ein Kontextwechsel zum nächsten Thread durchgeführt, auch wenn der Thread selbst noch nicht beendet ist. Welcher Thread wann an die Reihe kommt wird unter anderem durch Priorisierung bestimmt. Durch den rapiden Wechsel zwischen den Threads erscheint es, als würden mehrere Threads gleichzeitig ausgeführt werden – es handelt sich aber lediglich und Pseudo-Gleichzeitigkeit.

Switch on Event Multithreading (SoEMT)

Bei SoEMT wird der Kontextwechsel nicht nach einer vorgegebenen Zeit initiiert, sondern durch bestimmte Ereignisse, beispielsweise Nutzereingaben, den Zugriff des Programms auf das I/O-Register oder den Beginn einer speicherintensiven Operation, ausgelöst. Dadurch werden die Prozesse nicht ganz so rapide gewechselt und man spricht auch von Coarse-Grained Multithreading.

Simultanes Multithreading (SMT)

Bei simultanem Multithreading kann der Prozessor dank getrennten Pipelines oder zusätzlichen Registersätzen mehrere Threads ausführen. Es handelt sich bei SMT also um echte Gleichzeitigkeit. Eine Form von SMT ist die Hyper-Threading Technology (HTT), die von Intel-Prozessoren implementiert wird. Der physische Prozessor erscheint für das Betriebssystem wie zwei logisch getrennte CPUs, die physische Ressourcen miteinander teilen. Dadurch behandelt das Betriebssystem den physischen Prozessor wie zwei und teilt die Threads entsprechend auf. Der Prozessor selbst nutzt eine eigene Logik mit zwei parallelen Befehls- und Datenströmen, um die Ausführungsgeschwindigkeit zu steigern. 

Obwohl Hyper-Threading die Ausführung von Programmen durchaus beschleunigt, ist das Ergebnis durch das Vorhandensein mehrerer Kerne natürlich besser. Intel-Prozessoren haben zumeist mehrere physische Kerne, die jeweils Hyper-Threading verwenden.

Multiprocessing

Die bisher vorgestellten Varianten des Multithreading haben sich ausschließlich mit der Umsetzung auf einem Prozessorkern beschäftigt. Bei Multiprocessing werden zwei oder mehr Prozessoren innerhalb eines Systems verwendet. Multiprocessing ist daher ebenfalls eine Möglichkeit für echt-parallele Ausführung mehrerer Tasks oder Threads. Während des Kontextwechsels entsteht bei Multiprocessing wie auch bei SMT meist nur ein geringer Overhead. Lediglich, wenn die Anzahl der einzelnen Software-Threads die Anzahl an Threads, die die Hardware unterstützt, übersteigt, kann der Kontextwechsel teuer werden, denn nun kann der Thread nicht mehr dauerhaft die ihm zugehörigen CPU-Ressourcen (beispielsweise den Registersatz) behalten. 

Abgrenzung zwischen Multithreading und Multitasking

Multitasking ist ebenfalls ein Begriff aus der Informatik, der sich mit Nebenläufigkeit auseinandersetzt. Im Gegensatz zu Multithreading beschäftigt sich Multitasking allerdings mit der parallelen Ausführung verschiedener Prozesse, während bei Multithreading einzelne Ausführungsstränge innerhalb eines Prozesses betrachtet werden. Multithreading kann daher auch als threadbasiertes Multitasking bezeichnet werden. 

Beide Varianten der Nebenläufigkeit wechseln ständig zwischen den aktuell laufenden Programmen bzw. Threads. Bei Multitasking werden allerdings für jedes Programm eigene Ressourcen bereitgehalten. Beim Multithreading hingegen sind die Threads nicht voneinander abgeschottet, das heißt sie nutzen gemeinsame Ressourcen wie den Adressraum oder Handles auf Dateien. Die vorhin erwähnten Race Conditions können daher zwar bei Multithreading, nicht aber bei Multitasking auftreten. Da es aufwändiger ist, einen ganzen Prozess als einen Thread zu initialisieren, ist Multitasking allerdings ressourcenintensiver. 

Fazit

Multithreading bezeichnet die parallele oder pseudo-parallele Ausführung mehrerer Ausführungsstränge (sogenannter Threads), die zu einem Task oder Prozess gehören. Dadurch kann ein Programm zum einen besser auf Ereignisse wie Nutzereingaben reagieren und zum anderen Hardwareressourcen besser nutzen. Multithreading kann sowohl innerhalb der Software als auch durch die Hardware realisiert werden. Bei der softwareseitigen Umsetzung von Multithreading erscheint der Prozess von außen wie nur ein einziger Strang. Lediglich die Software selbst erzeugt den Eindruck von Nebenläufigkeit durch geschickte Thread-Priorisierung. Bei hardwareseitigem Multithreading sind auch von außen mehrere Threads sichtbar. Das Betriebssystem kann Pseudo-Gleichzeitigkeit durch rapide Kontextwechsel, beispielsweise nach Ablauf eines Zeitabschnitts (Time Slicing) oder auf bestimmte Ereignisse hin (Switch on Event Multithreading), erreichen. Echte Gleichzeitigkeit wird unter anderem durch das Vorhandensein mehrerer Prozessoren (Multiprocessing) oder die Bereitstellung mehrerer Register für unterschiedliche Threads auf einem Kern (Simultanes Multithreading) realisiert.

1 Kommentar zu “Multithreading

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.