Wikipedia · einfach zusammengefasst · Stand
Thread (Informatik)
Kritischer Abschnitt · Nebenläufigkeit · Parallele Programmierung · Prozess · Threadsicherheit. Literatur. Bearbeiten. Peter Ziesche: Nebenläufige & verteilte …
Inhalt6 Abschnitte
Grundidee und Aufbau
Ein Thread ist ein Ausführungsstrang, also eine bestimmte Reihenfolge von Befehlen bei der Abarbeitung eines Programms. Er ist stets Teil eines Prozesses. Der Artikel behandelt vor allem Kernel-Threads: Sie werden vom Betriebssystem gesteuert. User-Threads werden dagegen vollständig durch die Anwendersoftware verwaltet.
Mehrere Kernel-Threads eines Prozesses teilen sich wichtige Betriebsmittel, darunter Codesegment, Datensegment und Dateideskriptoren. Jeder Thread besitzt jedoch einen eigenen Threadkontext mit einem unabhängigen Registersatz einschließlich Befehlszeiger sowie einen eigenen Stapel (Stack), der meist im gemeinsamen Prozess-Adressraum liegt. Manche Ressourcen, etwa Thread-local storage oder ein Window-Handle, dürfen nur von einem bestimmten Thread verwendet werden.
Threads desselben Prozesses können verschiedenen Prozessoren oder Prozessorkernen zugeordnet werden. Dadurch lassen sich Programmaufgaben in überschaubare Einheiten zerlegen und umfangreiche Arbeiten parallel verteilen. Weil die Threads denselben Adressraum verwenden, können sie besonders einfach Daten austauschen. Die gemeinsame Nutzung kann aber Konflikte verursachen, die durch Synchronisationsmechanismen geregelt werden müssen.
Ein Thread kann typischerweise folgende Zustände haben: aktiv (running), wenn er Befehle auf der CPU ausführt; bereit (ready), wenn er rechenbereit ist, aber zugunsten eines anderen Threads wartet; blockiert (waiting), wenn er auf ein Ereignis oder einen Betriebssystemdienst wartet; sowie inaktiv, wenn er gerade eingerichtet wird, bereits beendet ist oder aus der Threadliste entfernt beziehungsweise wiederverwendet werden kann.
Abgrenzung zu Prozess, Task und User-Thread
Ein Prozess ist der Ablauf eines Computerprogramms auf einem oder mehreren Prozessoren. Ihm sind ein eigener Adressraum und weitere Betriebssystemmittel zugeordnet. Prozesse sind gegeneinander abgeschirmt: Greift ein Prozess auf nicht zugeteilte Adressen oder Ressourcen zu, schlägt der Zugriff fehl und der Prozess wird vom Betriebssystem abgebrochen. Ein Prozess kann einen oder mehrere Threads enthalten.
Threads eines Prozesses teilen Speicher, Prozessoren und betriebssystemabhängige Ressourcen wie Dateien und Netzwerkverbindungen. Ihre Verwaltung ist deshalb üblicherweise weniger aufwendig als die Verwaltung mehrerer Prozesse. Beim Threadwechsel ist kein vollständiger Wechsel des Prozesskontextes nötig, weil ein gemeinsamer Teil erhalten bleibt. Auch Kommunikation und Datenaustausch sind schneller und einfacher als zwischen getrennten Prozessen.
Task bezeichnet aus Sicht des Betriebssystems meist eine Aufgabe und wird häufig als Synonym für Prozess verwendet. In der Softwarearchitektur kann der Ausdruck jedoch allgemeiner eine zusammenhängende Aufgabe meinen und wird selten auch synonym zu Thread benutzt.
Ein User-Thread ist ein von der Anwendersoftware verwalteter Ausführungsstrang innerhalb eines Kernel-Threads; Microsoft verwendet dafür auch den Begriff Fiber. Seine Verwaltung übernimmt nicht das Betriebssystem, sondern allein das Anwendungsprogramm. User-Threads sind grundsätzlich unabhängig vom Betriebssystem möglich, sofern sich der vollständige Prozessorzustand auslesen und wiederherstellen lässt.
Bei modernen Systemen ist die Grenze zwischen Prozess und Kernel-Thread teilweise fließend. Linux erzeugt beide mit clone(2); dabei lässt sich fein festlegen, welche Ressourcen geteilt werden. CPU-Register und Stack bleiben ausgenommen.
Typische Systemmodelle und Anwendungen
Betriebssysteme und Programme können Prozesse, Kernel-Threads und User-Threads unterschiedlich kombinieren. MS-DOS führte ein Programm ohne diese Formen der Parallelität aus, sodass es zu einem Zeitpunkt nur eine Aktion bearbeiten konnte. Windows 3.1 ließ seine Programme dagegen als User-Threads in einem einfachen Prozess laufen; ein Programm konnte daher den Speicher eines anderen beschädigen.
Die ursprüngliche Implementierung von Amiga OS unterstützte Kernel-Threads, aber keine geschützten Prozesse. Das vermied den Zusatzaufwand des Speicherschutzes, hatte jedoch den Nachteil, dass ein Fehler einer Anwendung den gesamten Computer lahmlegen konnte. Viele ältere Unix-Implementierungen setzten dagegen auf voneinander geschützte Prozesse. Deren Informationsaustausch konnte mit Shared Memory fehlerträchtig oder mit Message Passing aufwendig sein; für asynchrone Aufgaben war der Systemaufruf fork() erforderlich.
Sun OS (Solaris) verwendete sogenannte Green Threads als kooperativ arbeitende User-Threads, obwohl Prozesse präemptiv verwaltet wurden. Dieses Modell kommt weiterhin bei Mikrocontrollern und eingebetteten Geräten vor.
Moderne Betriebssysteme wie Windows NT ab 3.51 SP3+, Windows 2000, Windows XP, Mac OS X und Linux unterstützen Prozesse und Kernel-Threads; Programme können zusätzlich User-Threads verwenden. Seit 1995 ist die Kombination aller drei Konzepte bei den meisten Betriebssystemen üblich. Ein typischer Einsatz besteht darin, eine grafische Benutzerschnittstelle zu bearbeiten, während das Programm zugleich auf Eingaben wartet oder Hintergrundarbeiten ausführt. Mehrere Kernel-Threads sind nötig, wenn eine Anwendung mehrere Prozessoren oder Prozessorkerne nutzen soll.
Umsetzung in Java und .NET
Java sieht Multithreading von Anfang an vor. Die Java Virtual Machine kann Threadumschaltung und Stackverwaltung selbst übernehmen oder die Threadfunktionen des Betriebssystems verwenden. Im Basispaket java.lang stellt die Klasse Thread die Verwaltungseinheit dar. Eine Anwendung kann Thread als Basisklasse verwenden oder eine Klasse mit der Schnittstelle java.lang.Runnable und einer Methode run() verbinden. thread.start() startet den Thread und führt run() aus; solange run() läuft, ist der Thread aktiv.
Mit wait() kann ein Java-Thread für eine bestimmte Zahl von Millisekunden oder unbegrenzt warten. notify() aus einem anderen Thread beendet dieses Warten. Beide Methoden gehören zu Object und müssen für zusammengehörige Kommunikation auf derselben Instanz organisiert werden. Kritische Abschnitte werden mit synchronized geschützt. Die früher vorhandenen Methoden suspend(), resume() und stop() gelten als deprecated: Wird ein Thread in einem kritischen Abschnitt von außen angehalten, können Deadlocks entstehen; wird er abgebrochen, können Daten inkonsistent bleiben.
.NET unterstützt Threads mit den Klassen aus System.Threading. Neben Prozess und Thread gibt es die Anwendungsdomäne AppDomain, einen von der Runtime isolierten „logischen Prozess“. Betriebssystemressourcen einschließlich Kernel-Threads sind jedoch nicht an diese Grenze gebunden. Ein eigener Thread erhält im Konstruktor eine Rückruffunktion (Delegate), wird mit Start() gestartet und endet bei deren Rückkehr.
Für kurze Hintergrundarbeiten bietet .NET einen verwalteten Threadpool. ThreadPool.QueueUserWorkItem() weist vorhandenen Threads Arbeit zu; danach werden sie zur Wiederverwendung gespeichert. Vordergrund- und Hintergrundthreads unterscheiden sich beim Prozessende: Sobald der letzte Vordergrundthread endet, wird der Prozess beendet und noch laufende Hintergrundthreads werden automatisch beendet. Threadpool-Threads sind Hintergrundthreads.
.NET synchronisiert Threads über WaitHandle und häufig über Monitor, das den Mutex eines .NET-Objekts verwendet. In C# dient dazu lock(object){ Anweisung; }. Die externe Steuerung über Abort(), Suspend() und Resume() kann Deadlocks oder Abbrüche einer AppDomain auslösen; Suspend und Resume sind deshalb in neueren .NET-Versionen als obsolet markiert.
Unix/Linux und Windows
Unix realisierte Parallelverarbeitung traditionell durch Prozesse, die mit fork erzeugt werden. Threads kamen später hinzu, zunächst ohne sichere Portabilität zwischen Unix-Derivaten. POSIX-Threads beziehungsweise die Native POSIX Thread Library legten schließlich einen einheitlichen Mindestfunktionsumfang und eine gemeinsame API fest, die aktuelle Linux-Versionen als NPTL unterstützen. Ein Thread wird gegenüber einem Prozess auch Leichtgewichtprozess genannt, weil das Betriebssystem Threads desselben Prozesses mit weniger Rechenaufwand umschalten kann.
Unter Windows können C- und C++-Programme einen Thread über die Windows-API mit CreateThread erzeugen. Dabei werden unter anderem die auszuführende Subroutine, ein Parameter und ein Zeiger für die threadId übergeben. CreateThread liefert ein HANDLE, über das weitere Operationen möglich sind. Endet die Threadfunktion, endet auch der Thread. Mit SetThreadPriority kann seine Priorität verändert und mit GetExitCodeThread sein Rückgabewert abgefragt werden.
Für objektorientierte C++-Programme kann die Threadfunktion einen übergebenen Zeiger in eine abstrakte Basisklasse wie Runnable umwandeln und deren virtuelle run()- beziehungsweise Fachmethode aufrufen. Dadurch wird ähnlich wie bei Java die gewünschte Methode einer konkreten Anwenderklasse dynamisch gebunden. Das verwendete Objekt darf allerdings nicht zerstört werden, solange der neue Thread noch darauf zugreift; dafür ist Threadsynchronisation erforderlich.
Risiken und Modellierung mit UML
Nebenläufige Programme mit Threads sind schwierig zu entwickeln, weil ihr Ablauf nicht mehr rein sequenziell ist. Der Scheduler bestimmt die Reihenfolge und den Wechsel zwischen Threads, worauf die Entwicklung nur begrenzten Einfluss hat. Selbst bei Mutexen und Semaphoren kann ein Programm in ungeplante Gesamtzustände geraten. Mögliche Folgen sind Deadlocks, bei denen sich Ausführungsstränge gegenseitig dauerhaft blockieren, Livelocks, bei denen sie aktiv bleiben, aber keinen Fortschritt erzielen, außerdem Datenfehler und Abstürze. Da solche Probleme oft nur sporadisch auftreten, sind sie schwer zu reproduzieren und zu finden.
In der Unified Modeling Language (UML) werden parallele Abläufe häufig durch Zustandsdiagramme (Statecharts) dargestellt. Innerhalb eines Zustands können parallele Teil-Zustandsdiagramme liegen. Das Gesamtsystem arbeitet diese quasiparallel ab: Die einzelnen Zustandsübergänge dauern meist nur wenige Mikrosekunden bis Millisekunden, sodass ihre sequenzielle Ausführung parallel erscheint. Ereignisse aus einer Eventqueue lösen Übergänge aus; ein solcher ereignisgesteuerter Übergang entspricht hier einem User-Thread. Diese Form der Parallelität kann mit nur einem Betriebssystem-Thread auskommen.
Wenn Zustandsübergänge länger dauern, auf Bedingungen oder Ein-/Ausgabe warten oder zeitlich priorisiert werden müssen, ist echte Aufteilung auf mehrere Threads erforderlich. Das UML-Werkzeug Rhapsody verwendet dafür aktive Klassen, denen jeweils ein eigener Thread zugeordnet ist.
Alternativ kann ein an Java angelehntes Modell mit einer ausdrücklichen Thread-Klasse verwendet werden. Eine run()-Methode kann dabei in einer Schleife auf Daten warten, Arbeiten ausführen, Bedingungen prüfen und andere Threads mit notify() benachrichtigen. UML zeigt dann die Anwenderklasse, die Thread-Klasse und ihre Beziehungen in einem Klassendiagramm, gegebenenfalls ergänzt durch Sequenzdiagramme. Für solche hochzyklischen Abläufe bietet ein Zustandsdiagramm laut Artikel keine besseren grafischen Möglichkeiten.
Lernvideos zu Thread (Informatik)
2:37
Smart Home mit Matter® over Thread: Welche Systemvoraussetzungen brauche ich? | WAGO Home Automation
WAGO Germany · 1.646 Aufrufe
7:09
Betriebssysteme #14 - Einführung Threads, Eigenschaften von Threads, Vorteile, Thread Typen deutsch
IT BREAK · 790 Aufrufe
11:34
Threading Tutorial #1 - Concurrency, Threading and Parallelism Explained
Tech With Tim · 254.691 Aufrufe