Thread (Informatik)

Ausführungsstrang oder eine Ausführungsreihenfolge in der Abarbeitung eines Programms
Dies ist eine alte Version dieser Seite, zuletzt bearbeitet am 4. September 2006 um 22:12 Uhr durch Ar-ras (Diskussion | Beiträge) (Bedeutungsunterschied Thread gegenüber Prozess, Task und Fiber). Sie kann sich erheblich von der aktuellen Version unterscheiden.

Ein Thread (auch Aktivitätsträger), in der deutschen Literatur vereinzelt auch als Faden bezeichnet, ist in der Informatik ein Ausführungsstrang beziehungsweise eine Ausführungsreihenfolge der Abarbeitung der Software.

Bedeutungsunterschied Thread gegenüber Prozess, Task und Fiber

Ein Prozess bezeichnet den Ablauf eines Computerprogrammes auf einem Prozessor. Ein Prozess kann genau einen Thread des Betriebssystems belegen, wenn bei dem Programmablauf keine Parallelverarbeitung vorgesehen ist. Unter Unix gibt es von je her einfach zu beherrschende Systemaufrufe zur Erzeugung paralleler Prozesse (fork), mit diesem Mittel wird unter Unix/Linux traditionell die Parallelverarbeitung realisiert. Threads sind erst in neueren Unix/Linux-Versionen eingeführt. Gegenüber einem Prozess wird ein Thread auch als Leichtgewichtprozess bezeichnet, da die Umschaltung zwischen Prozessen mehr Aufwand (Rechenzeit) im Betriebssystem erfordert als die Umschaltung zwischen Threads eines Prozesses. Einem Prozess ist ein Speicherraum und weitere Betriebssystemmittel zugeordnet, mehrere Threads eines Prozesses teilen sich hingegen die Betriebsmittel des Prozesses und greifen auf den selben Speicherraum zu.

Bereits in den 80er Jahren gab es sogenannte Multitask-Betriebssysteme, da im Unterschied zu den joborientierten Systemen insbesondere in der damals so bezeichneten Prozessrechentechnik mehrere Aufgaben quasi parallel ausgeführt werden mussten. Damals hat sich der Begriff Task für eine Aufgabe aus Sicht des Betriebssystems eingebürgert, das ist synonym zu dem hier beschriebenem Thread. Der Begriff Task (deutsch: Aufgabe) wird in der Softwarearchitektur aber auch unscharf allgemein für zusammenhängende Aufgaben benutzt, insbesondere auch synonym für Prozess eingesetzt.

Ein Thread ist wörtlich ein einzelner Ausführungsfaden eines Programmes, der Begriff Thread wird aber auf den Ausführungsstrang aus Sicht des Betriebssystems eingesetzt. In einer Anwendersoftware kann dieser Ausführungsstrang durch geeignete Programmierung nochmals in unabhängige Einzelstränge unterteilt werden, das geschieht beispielsweise bei der Abarbeitung verschiedener in UML notierter Statecharts im selben Betriebssystem-Thread. Im englischen Sprachraum hat sich für den einzelnen Ausführungsstrang der Anwendersoftware der Begriff Fiber (deutsch: Faser) eingebürgert. Die folgende Tabelle ist aus der englischen Wikipedia kopiert und ohne Bedeutungsänderung übersetzt und zeigt die Begriffsunterschiede. (Achtung: Die Tabelle enthält einige Links aus der original englischen Wikipedia, die hier nicht funktionieren. Es muss entschieden werden, welche Links eingedeutscht oder beibehalten werden.)

In der Tabelle bedeuten P Prozess, T Thread und F Fiber.

P T F Beispiel
Nein
Nein
Nein
Ein Computerprogramm unter MS-DOS ablaufend. Das Programm kann zu einer Zeit nur eine Aktion ausführen.
Nein
Nein
Ja
Windows 3.1 auf der Oberfläche von DOS. Alle Windows-Programme laufen in einem einfachen Prozess, ein Programm kann den Speicher eines anderen Programmes zerstören, was den bekannten General Protection Fault (Allgemeine Schutzverletzung) zur Folge hat.
Nein
Ja
Nein
Amiga OS's ursprüngliche Implementierung. Das Betriebssystem unterstützt vollständig Threads und erlaubt dabei, dass mehrere Applikationen unabhängig voneinander ablaufen, vom Betriebssystemkern scheduled. Wegen der nichtvorhandenen Prozess-Unterstützung ist das System effizienter (wegen der Vermeidung des Zusatzaufwandes des Speicherschutzes), mit dem Preis, dass Applikationsfehler den gesamten Computer lahmlegen (crash) können.
Nein
Ja
Ja
Mac OS 9 unterstützt Fibers mittels Apple's Thread Manager und Threads mittels Apple's Multiprocessing Services, das mit dem nanokernel arbeitet, eingeführt in Mac OS 8.6. Damit werden Threads unterstützt, aber immer noch der MultiFinder's Weg des Verwaltens von Applikationen benutzt.
Ja
Nein
Nein
Die meisten bekannten Implementierungen von Unix. Das Betriebssystem kann mehr als ein Programm zu einem Zeitpunkt ausführen, die Programmabarbeitungen sind gegeneinander geschützt. Wenn ein Program sich falsch verhält, kann es seinen eigenen Prozess stören, was das Beenden dieses einen Prozesses zur Folge haben kann, ohne dass das Betriebssystem oder andere Prozesse gestört werden. Jedoch kann der Informationsaustausch zwischen Prozessen entweder fehlerbehaftet sein (bei Nutzungen von Techniken wie shared memory) oder aufwändig (bei Nutzung von Techniken wie message passing). Die asynchrone Ausführung von Aufgaben benötigt einen aufwändigen fork() Systemaufruf.
Ja
Nein
Ja
Sun OS von Solaris. Sun OS ist Sun Microsystem's Version von Unix. Sun OS implementiert sogenannte green threads um einem einfachen Prozess die asynchrone Ausführung mehrerer Aufgaben zu ermöglichen, beispielsweise playing a sound, repainting a window, oder auf ein Bediener-Ereignis zu reagieren wie die Anwahl des stop button. Obwohl die Prozesse präemptiv verwaltet werden, sind die green threads oder Fibers kooperativ arbeitend. Dieses Modell wird oft anstelle von richtigen Threads genutzt. Dieses Modell ist immer noch aktuell in Mikrocontroller- und sogenannten embedded devices.
Ja
Ja
Nein
Das ist der allgemeine Fall der Applikationen unter Windows NT ab 3.51 SP3+, Windows 2000, Windows XP, Mac OS X, Linux, und anderen modernen Betriebssystemen. Obwohl alle diese Betriebssysteme den Programmierer die Nutzung von Fibers beziehungsweise von Bibliotheken, die mit Fibers arbeiten, erlauben, nutzen die meisten Programmierer die Fibers nicht in ihren Anwendungen. Die Programme sind multithreaded und laufen unter einem multitasking operating system, aber führen keine user-level - Umschaltung (context switching) aus.

Auf gewöhnlichen PCs verwenden die meisten Prozesse zwei oder mehrere Threads. Einige wenige Prozesse verwenden nur einen einzigen Thread. Diese Prozesse sind gewöhnlich Dienste die ohne Interaktion mit dem Benutzer arbeiten. Normalerweise gibt es keine Prozesse welche Fiber verwenden.

Ja
Ja
Ja
Die meisten Betriebssysteme seit 1995 fallen in diese Kategorie. Die Nutzung von Threads zur gleichzeitigen Ausführung ist die gewöhnliche Auswahl, obwohl es auch multi-process und multi-fiber - Applikationen gibt. Diese werden beispielsweise benutzt, damit ein Programm seine grafische Benutzerschnittstelle abarbeiten kann während es gleichzeitig auf eine Eingabe des Benutzers wartet oder eine Rechtschreibprüfung ausführt.

Thread aus Sicht des Betriebssystems

Auf Betriebssystemen, die Threads unterstützen, ist der Verwaltungsaufwand für Threads üblicherweise geringer als der für Prozesse. Ein wesentlicher Effizienzvorteil von Threads besteht darin, dass im Gegensatz zu Prozessen beim Threadwechsel kein vollständiger Austausch des Prozesskontextes notwendig ist, da alle Threads einen gemeinsamen Teil des Prozesskontextes verwenden.

 

Das Bild zeigt die beschriebenen Zusammenhänge im Detail. Ein Thread teilt sich mit den anderen vorhandenen Threads des zugehörigen Prozesses eine Reihe von Betriebsmitteln, nämlich das Codesegment, das Datensegment und die verwendeten Dateideskriptoren. Allerdings bewahrt jeder Thread seinen eigenen Befehlszähler und seinen eigenen Stack. Durch die gemeinsame Nutzung des Speicherbereichs kann es natürlich auch zu Konflikten kommen. Diese müssen durch den Einsatz von Synchronisationsmechanismen aufgelöst werden. Die Gesamtheit zusammengehöriger Threads inklusive ihrer Betriebsmittel bezeichnet man als Task.

Da Threads, die dem selben Prozess zugeordnet sind, den gleichen Adressraum verwenden, ist eine Kommunikation zwischen diesen Threads von vorneherein möglich (vgl. mit Interprozesskommunikation bei Prozessen).

Threads innerhalb des gleichen Prozesses verwenden voneinander unabhängige Stapel (Stacks), die unterschiedlichen Abschnitten des Adressraums zugeordnet sind.

Jeder „Programmfaden“ ist für die Ausführung einer bestimmten Aufgabe verantwortlich. Die Ausführungsstränge der Programmfunktionen können damit in überschaubare Einheiten aufgeteilt werden.

Bei den meisten Betriebssystemen kann ein Thread neben dem Zustand inaktiv die Zustände rechnend (engl. running), rechenbereit (engl. ready) und blockiert (engl. waiting) annehmen. Im Zustand rechnend findet die Ausführung von Befehlen auf der CPU statt, bei rechenbereit ist der Thread gestoppt, um einen anderen Thread rechnen zu lassen und bei blockiert wartet der Thread auf ein Ereignis.

Threads in Java

Die Programmiersprache Java ist auf fast allen Plattformen lauffähig und kann als allgemein bekannt angesehen werden. Daher soll Java zur Illustration der Threadverarbeitung herangezogen werden. In Java ist ein Arbeiten mit mehreren Threads von vornherein vorgesehen. Dabei funktioniert das Multithreading auch, wenn das Betriebssystem dieses nicht oder nur mangelhaft unterstützt. Möglich ist das, weil die Virtuelle Maschine des Java die Threadumschaltung einschließlich Stackverwaltung übernehmen kann. In Betriebssystemen mit Threadunterstützung können die Betriebssystemeigenschaften direkt genutzt werden. Die Entscheidung darüber liegt in der Programmierung der Virtuellen Maschine.

In Java gibt es im Basis-Package java.lang die Klasse Thread. Instanzen von dieser Klasse sind Verwaltungseinheiten der Threads. Thread kann entweder als Basisklasse für eine Anwenderklasse benutzt werden, oder eine Instanz von Thread kennt eine Instanz einer beliebigen Anwenderklasse. Im zweiten Fall muss die Anwenderklasse das Interface java.lang.Runnable implementieren und demzufolge eine Methode run() enthalten.

Ein Thread wird gestartet mittels Aufruf von thread.start(). Dabei wird die zugeordnete run()-Methode abgearbeitet. Solange run() läuft, ist der Thread aktiv.

In der Methode run() oder in den von dort gerufenen Methoden kann der Anwender mittels wait() den Thread eine Zeit (in Millisekunden angegeben) oder auch beliebig lange warten lassen. Dieses Warten wird aber mit einem notify() aus einem anderen Thread beendet. Das ist ein wichtiger Mechanismus der Inter-Thread-Kommunikation. wait() und notify() sind Methoden der class Object und auf alle Instanzen von Daten anwendbar. Zueinandergehörige wait() und notify() sind an der selben Instanz (einer Anwenderklasse) zu organisieren, sinnvollerweise werden in dieser Instanz dann auch die Daten übergeben, die ein Thread dem anderen mitteilen möchte.

Die Realisierung von kritischen Abschnitten erfolgt mit synchronized.

In der ersten Version von Java wurden Methoden der class Thread zur Unterbrechung eines Threads von außen, Fortsetzung und Abbruch eingeführt: suspend(), resume() und stop(). Diese Methoden wurden aber recht schnell in Nachfolgeversionen als Deprecated (missbilligt) bezeichnet. In der ausführlichen Begründung wurde ausgeführt, dass ein System unsicher ist, wenn ein Thread von außen angehalten oder abgebrochen werden kann. Die Begründung mit wenigen Worten ist folgende: Ein Thread kann sich möglicherweise in einer Phase eines kritischen Abschnittes befinden und Daten teilweise geändert haben. Wird er angehalten, dann ist der kritische Abschnitt blockiert und deadlocks sind die Folge. Wird er abgebrochen und die Blockierung vom System aufgehoben, dann sind Daten inkonsistent. An dieser Stelle kann ein Laufzeitsystem nicht selbst entscheiden, ein Anhalten oder Abbruch eines Threads kann nur das Anwenderprogramm selbst steuern.

Threads in Windows

Um in Windows in C oder C++ einen eigenen Thread zu erzeugen, kann man direkt auf die Windows-API-Schnittstellen zugreifen. Dazu muss man als einfaches Muster aufrufen:

#include <winbase.h>
unsigned long threadId;
HANDLE hThread = CreateThread(NULL, 2000, (LPTHREAD_START_ROUTINE)runInThread, p1, p2, &threadId);

runInThread ist die Subroutine, die im diesem Thread laufen soll, sie wird unmittelbar danach aufgerufen. Wird runInThread beendet, dann ist auch der Thread beendet, ähnlich wie Thread::run() in Java.

Diese API-Schnittstelle ist eine C-orientierte Schnittstelle. Um Threads auch objektorientiert zu programmieren, kann nach folgendem Schema in der Subroutine runInThread eine Methode einer Klasse gerufen werden:

void runInThread(void* runnableInstance)
{ Runnable* runnable = (Runnable*)(runnableInstance);  //Klassenzeiger oder Zeiger auf Basisklasse
  runnable->run();                                     //run-Methode dieser Klasse wird gerufen. 
}

Diejenige Klasse, die die run()-Methode für den Thread enthält, ist hier in einer Klasse Runnable enthalten, das kann auch einen Basisklasse einer größeren Klasse sein. Der Zeiger auf die Instanz einer gegebenenfalls von Runnable abgeleiteten Klasse muss bei CreateThread als Parameter p1 übergeben werden, und zwar als (Runnable*) gecastet. Damit hat man hier die gleiche Technik wie bei Java in der Hand. Wie folgt wird die universelle Basisklasse (ein Interface) für alle Klassen, deren run()-Methoden in einem eigenem Thread laufen sollen, definiert:

class Runnable             //das ist ein Interface
{  virtual void run()=0;
};

Folgend wird die Anwenderklasse, mit der run()-Methode definiert:

class MyThreadClass : public Runnable
{  virtual void run();
   void weitereMethode();
};

Folgend wird die Anwenderklasse instanziiert und der Thread gestartet:

MyThreadClass* myThread = new MyThreadClass();
hThread = CreateThread(NULL, 2000, (LPTHREAD_START_ROUTINE)runInThread, (Runnable*)(myThread), 0, &threadId);

Wegen des dynamischen Bindens wird die gewünschte Methode myThread->run() gerufen:

void MyThreadClass::run()  
{ weitereMethode();        //also sind auch alle weiteren Dinge dieser class aufrufbar.
}

Weitere Zugriffe auf den Thread auf API-Ebene können unter Kenntnis des zurückgelieferten HANDLE ausgeführt werden, beispielsweise

 SetThreadPriority(hThread, 3);


Threaddarstellung in UML

Parallele Prozesse werden in der Unified Modeling Language (UML) oft mit Zustandsdiagrammen (Statecharts) dargestellt. In einem Zustandsdiagramm sind innerhalb eines Zustandes interne parallele Teil-Zustandsdiagramme darstellbar. Alle Zustandsdiagramme des Gesamtsystems werden quasiparallel angearbeitet. Die Parallelität wird dadurch erreicht, dass jeder Zustandsübergang atomar kurz sein soll (in der Praxis in wenigen Mikrosekunden bis Millisekunden abgearbeitet ist) und daher das Nacheinander der Abarbeitung als parallel erscheint. Der Übergang von einem State in einen anderen wird typischerweise mit einem Ereignis (Event) ausgelöst, das zuvor in die sogenannte Eventqueue eingeschrieben wurde. Dieser Übergang aufgrund eines Ereignisses ist nach der oben angegebenen Definition ein Fiber. Prinzipiell ist die damit realisierte Parallelität mit nur einem einzigen Betriebssystem-Thread erreichbar.

Setzt man UML für schnelle Systeme ein, dann spielt die Frage einer zeitlichen Priorisierung eine Rolle. Können Zustandsübergänge längere Zeit in Anspruch nehmen oder es soll in einem Übergang noch zusätzlich auf Bedingungen gewartet werden (passiert bereits bei einem einfachen Lesen oder Schreiben auf eine Datei), dann muss eine Parallelität mit Threads realisiert werden. Aus diesem Grunde muss man die Zustandsdiagramm-Abarbeitung mehreren gegebenenfalls unterschiedlich prioren Threads des System zuordnen können. Das UML-Werkzeug Rhapsody kennt dazu den Begriff der aktiven Klasse. Jede aktiven Klasse ist einem eigenem Thread zugeordnet.

Zusätzlich zur Formulierung von Parallelarbeit mit Statecharts kann auch in UML-entworfenen Systemen eine Parallelität mit Threads modelliert werden. Es kann dazu das Programmier-Modell verwendet werden, das Java bietet. In diesem Fall ist im Anwender-Modell eine explizite Klasse Thread mit den in Java bekannten Eigenschaften einzubringen. Damit sind hochzyklische Probleme einfacher und effektiver zu beherrschen, wie das folgende Beispiel zeigt:

void run()
{ while(not_abort)          //zyklisch bis zum Abbruch von aussen
  {
    data.wait();            //der Zyklus beginnt wenn Daten vorliegen
    dosomething();          //Abarbeitung mehrerer Dinge
    if(condition)
    { dotheRightThing();    //Abarbeitung ist von Bedingungen abhängig
      partnerdata.notify(); //andere Threads benachrichtigen
    }
  } 
}

Die hier gezeigte Methode run() ist eine Methode einer Anwenderklasse, in ihr ist die gesamte Abarbeitung im Thread wie bei funktionalen Abarbeitungen auch in der UML üblich in Programmzeilen beschrieben. Die UML wird benutzt, um diese Anwenderklasse, die zugehörige Klasse Thread und deren Beziehungen zu zeigen (Klassendiagramm), ergänzt beispielsweise mit Sequenzdiagrammen. Die Programmierung ist übersichtlich. Ein Zustandsdiagramm bietet für diesen Fall keine besseren grafischen Möglichkeiten.


Siehe auch

Literatur

  • Peter Ziesche: Nebenläufige & verteilte Programmierung, W3L, 2004, ISBN 3937137041
  • Marcus Roming, Joachim Rohde: Assembler - Grundlagen der Programmierung. MITP-Verlag, ISBN 3-8266-0671-X
  • Olaf Neuendorf: Windows Multithreading mit C++ und C#. MITP-Verlag, ISBN 3-8266-0989-1
  • Heinz Kredel, Akitoshi Yoshida: Thread- und Netzwerk-Programmierung mit Java. Dpunkt Verlag, ISBN 3-89864-133-3
  • Rainer Oechsle: Parallele Programmierung mit Java Threads. Hanser Fachbuchverlag, ISBN 3-446-21780-0
  • Andrew S. Tanenbaum: Moderne Betriebssysteme. Pearson Studium Verlag, ISBN 3-8273-7019-1
  • Hans Joachim Müschenborn: OS/2 Systemprogrammierung. Multitasking – Interprozesskommunikation – Multithreading – DB/2-Integration. tewi-Verlag, ISBN 3-89362-344-2