Advanced Java Services | Join() und Yield() |
Wenn ein Programm startet gibt es nur einen Thread, den Main-Thread. Von da aus können weitere Threads erzeugt werden. In diesem Zusammenhang ist der Main-Thread der Elternthread und die weiteren Threads sind Kindthreads., Die Bezeichnung Thread für die C#-Klasse System.Threading.Thread führt zu einer begrifflichen Ungenauigkeit. Man muß nämlich unterscheiden zwischen einem Objekt vom Typ Thread und dem von diesem Objekt mit Hilfe der Methode Start() erzeugten Thread. Zunächst wird ein Objekt vom Typ Thread erzeugt, dies geschieht zwangsläufig in irgendeinem laufenden Thread, sagen wir in Main, dann wird mit Start() ein neuer Thread erzeugt und die übergebene Methode läuft dann in diesem Thread, d.h. das C#-Objekt und die dem Konstruktor übergebene Methode existieren (immer) in verschiedenen Threads, was ja auch die Screenshots zu den einführenden Beispielen bestätigen.
Mit diesen einfach zu verwendeten Methoden kann ein Thread A auf die Beendigung eines anderen Threads B warten,
etwa um das Ergebnis von B zu übernehmen. Join() blockiert also den weiteren Ablauf von A. Will man nicht
eine unbestimmte Zeit warten, so kann man die Wartezeit begrenzen, in dem man eine maximale Wartezeit in Millisekunden übergibt
oder auch in Form einer Instanz der Struktur TimeSpan
(siehe TimeSpan
Um auf einen anderen Thread warten zu können muß der Wartende den Thread als C#-Objekt kennen. Das folgende Beispiel
demonstriert die bisher erwähnten Eigenschaften.
Wir realisieren die vom Thread abzuarbeitende Methode in einer eigenen Klasse. Eine Instanz Worker erhält eine Liste von Strings über den Konstruktor. Die Methode TrimList aus Worker entfernt alle Leerzeichen aus den Strings und wandelt die Strings in Großschrift um. Sowohl der Thread als auch Main verwenden die statische Property CurrentThread() mit der man jederzeit feststellen kann, in welchem Thread ein Programmteil abläuft.
Die Klasse Worker sieht folgendermaßen aus.
class Worker { private List<string> listData; public Worker(List<string> listData) { this.listData = listData; Console.WriteLine("Konstruktor Worker\n"); } public void TrimList() { ConsoleColor defaultColor = Console.ForegroundColor; Console.ForegroundColor = ConsoleColor.DarkRed; Console.WriteLine("start thread"); for (int i = 0; i < listData.Count; i++) { string item = listData[i]; Console.WriteLine(Thread.CurrentThread.Name + " works with " + item); listData.Remove(item); item = item.Trim().ToUpper(); listData.Insert(i, item); Thread.Sleep(200); } Console.WriteLine("end thread\n"); Console.ForegroundColor = defaultColor; } }
Die Mainklasse legt ein Array von Strings an, verwandelt sie in eine Liste und übergibt diese dem Konstruktor von Worker.
public static void Main(string[] args) { Console.WriteLine("start main"); Thread.CurrentThread.Name = "Main"; string[] arr = { " alpha ", " beta ", " gamma " }; List<string> list = new List<string>(arr); Worker worker = new Worker(list); Thread th = new Thread(worker.TrimList); th.Name = "Worker"; th.Start(); th.Join(); //Thread.Sleep(200); Console.Write("main received: "); foreach (string st in list) Console.Write(st); Console.WriteLine("\nend main"); Console.WriteLine(); }
Main wird nun in zwei Varianten ablaufen.
In diesem Fall wartet Main bis der Thread zu Ende ist, was ca. 600 Millisekunden dauert und damit ist die Aussage voraussagbar.
In diesem Fall ist die Sleep()-Zeit in Main so gewählt, daß Main die Liste ausgibt, wenn der Thread das erste Element der Liste bearbeitet hat, es kann aber sein, daß er bereits zwei Elemente bearbeitet hat. Die Ausgabe in Main ist somit nicht genau vorhersehbar.
Mit Yield() (to yield - abgeben, abtreten) gibt ein Thread bekannt, daß er auf die Abarbeitung durch den Prozessor verzichtet. Er verhält sich somit fair und gibt anderen wartenden Prozessen eine Chance. Er wird dann vom Scheduler sofort wieder in die Warteschlange eingereiht und bewirbt sich daher erneut um den Prozessor. Allerdings ist Yield() nicht zwingend, es kann sein, daß das OS nicht zu einem anderen Thread wechselt. daher gibt Yield true oder false zurück, je nachdem ob es erfolgreich war oder nicht.
Meistens wird jedoch ein Yield() berücksichtigt wie das folgende kleine Beispiel zeigt.
Ein Thread gibt zehnmal seinen Namen aus und teilt mit, daß er sich nach jeder Ausgabe kurz zurückziehen möchte. Main legt zwei Threadobjekte an, vergibt als Namen die Ziffern 1 und 2 und startet die Threads. Zum Vergleich werden die Ausgaben bei auskommentiertem yield() gezeigt.
public static void Run() { for(int i=0; i < 10; i++) { Console.Write(Thread.CurrentThread.Name); Thread.Yield(); } }
private static void Main() { Thread th1 = new Thread(Run); th1.Name = "1"; Thread th2 = new Thread(Run); th2.Name = "2"; th1.Start(); th2.Start(); }
Eine Garantie für dieses Verhalten gibt es aber nicht. Trotz auskommentiertem Yield() kann es auch mal anders kommen...