Advanced Java Services | Datenübergabe an Threads |
Ein Thread hat natürlich Zugriff auf alle Daten, die in der gerufenen Methode angelegt werden. Darüber hinaus hat er Zugriff auf Daten, die in der Klasse liegen, in der die Methode liegt, wobei hier zu beachten ist, ob die Methode statisch oder nichtstatisch ist. des weiteren können Daten über den Parameter der Methode übergeben werden.
In C# erhält der Threadkonstruktor ein Delegate auf eine Methode, die dann in einem eigenen Thread ausgeführt wird. Da dem Threadkonstruktor auch ein Delegate übergeben werden kann, das für eine Methode mit einem Parameter vom Typ Object steht, ist dies eine erste Möglichkeit Daten an einen Thread zu übergeben.
Es soll ein StringBuilder verwendet werden, der von zwei Threads gefüllt wird. Da der Parameter von Methoden die dem Threadkonstruktor übergeben werden immer vom Typ Object ist, stellt das kein Problem dar.
private static void AppendChar(Object sb) { if (sb is StringBuilder) { for (int i = 0; i < 10; i++) { ((StringBuilder)sb).Append((char)('a' + i)); Thread.Sleep(new TimeSpan(1)); } } }
private static void AppendDigit(Object sb) { if (sb is StringBuilder) { for (int i = 0; i < 10; i++) { ((StringBuilder)sb).Append((char)('0' + i)); Thread.Sleep(new TimeSpan(1)); // 1 tick = 100 Nanosekunden } } }
private static void Main(string[] args) { StringBuilder sb = new StringBuilder(); Thread th1 = new Thread(AppendChar); Thread th2 = new Thread(AppendDigit); th1.Start(sb); th2.Start(sb); th1.Join(); th2.Join(); Console.WriteLine(sb); }
Da die beiden Threads dasselbe StringBuilderobjekt bearbeiten fallen die Ergebnisse recht unterschiedlich aus.
Hier drei Abläufe.
Genauer gesagt: Datenübergabe über den Konstruktor der Klasse, in der die zu rufende Methode liegt. Die Methode greift also auf Daten der Klasse zu, in der sie liegt.
Der folgenden Klasse werden über den Konstruktor Daten übergeben, die von der Methode DoWork() manipuliert werden.
class Worker { private StringBuilder sb; private char ch; public Worker(StringBuilder sb, char ch) { this.sb = sb; this.ch = ch; } public void DoWork() { for (int i = 0; i < 15; i++) { sb.Append(ch); Thread.Sleep(new TimeSpan(1)); } } }
Zwei Threads greifen auf diese Methode zu.
private static void Main(string[] args) { StringBuilder sb = new StringBuilder(); Worker w1 = new Worker(sb, '1'); Worker w2 = new Worker(sb, '2'); Thread th1 = new Thread(w1.DoWork); Thread th2 = new Thread(w2.DoWork); th1.Start(); th2.Start(); th1.Join(); th2.Join(); Console.WriteLine(sb); }
Durch die kleine Pause erhöht sich die Wahrscheinlichkeit, daß die Threads den StringBuilder nicht exklusiv erhalten.
Drei Ausgaben
Das erste Beispiel wird nun mit Hilfe von Lambda-Ausdrücken geschrieben.
private static void beispiel02() { StringBuilder sb = new StringBuilder(); Thread th1 = new Thread((Object sb) => { if (sb is StringBuilder) { for (int i = 0; i < 10; i++) { ((StringBuilder)ob).Append((char)('a' + i)); Thread.Sleep(new TimeSpan(1)); } } }); Thread th2 = new Thread((Object sb) => { if (sb is StringBuilder) { for (int i = 0; i < 10; i++) { ((StringBuilder)ob).Append((char)('0' + i)); Thread.Sleep(new TimeSpan(1)); } } }); th1.Start(sb); th2.Start(sb); th1.Join(); th2.Join(); Console.WriteLine(sb); }
Durch die Verwendung eines Lambdaausdrucks kann man den obigen Code noch deutlich vereinfachen. Der Lambdarumpf kann nämlich direkt auf Variablen der einhüllenden Methode zugreifen. Ein Lambdaausdruck, der auf Variablen der ihn einhüllenden Methode zugreift wird auch Closure genannt.
private static void Main(string[] args) { StringBuilder sb = new StringBuilder(); Thread th1 = new Thread(() => { for (int i = 0; i < 10; i++) { sb.Append((char)('a' + i)); Thread.Sleep(new TimeSpan(1)); } }); Thread th2 = new Thread(() => { for (int i = 0; i < 10; i++) { sb.Append((char)('0' + i)); Thread.Sleep(new TimeSpan(1)); } }); th1.Start(); th2.Start(); th1.Join(); th2.Join(); Console.WriteLine(sb); }
Das folgende einfachere Beispiel zeigt noch einmal das Vorgehen
private static void Main(string[] args) { int a = 12, b = 13, c = 14; Thread thread = new Thread( () => { SomeMethod(a, b, c); }); thread.Start(); } public static void SomeMethod(int a, int b, int c) { Console.WriteLine("{0} {1} {2}", a, b, c); }
Die Brüder Albahari bringen in ihrem Buch C# in a Nutshell (4.Ausgabe, S. 794) zur Warnung das folgende Beispiel
private static void Albahari01() { for(int i=0; i<10 ; i++) { Thread th = new Thread(() => Console.Write(i) ); th.Start(); } }
Hier werden 10 Threads gestartet, die auf die Laufvariable einer Schleife zugreifen. Es ist nicht vorhersehbar, wann die einzelnen Threads auf die Laufvariable zugreifen.
Das Ergebnis kann durchaus mal so aussehen
aber auch so
oder so
Als Lösung bietet das Buch an, die Threads auf eine temporäre Variable im Schleifenrumpf zuzugreifen, die die Laufvariable zwischenzuspeichern.
private static void Albahari02() { for(int i=0; i<10 ; i++) { int tmp = i; Thread th = new Thread(() => Console.Write(tmp) ); th.Start(); } }
Leider führt auch das nicht zum gewünschten Ergebnis, wie die folgenden Screenshots zeigen.