In diesem Teil der Artikelserie möchte ich Ihnen an einem Beispiel zeigen welche Auswirkungen die Speicherverwaltung im .NET Framework auf den Programmfluss haben kann. Der Compiler, die CLR und auch die CPU nutzen Caching Mechanismen zur Zwischenspeicherung von Variablenwerten. Wird ein Variablenwert mehrfach im Programmfluss benötigt dann kann der Wert direkt aus dem Cache gelesen werden. Dies erhöht die Geschwindigkeit der Programmausführung. In Multithreading Szenarien kann diese Geschwindigkeitsoptimierung aber leider unerwünschte Seiteneffekte haben.
Betrachten wir dazu die folgende Beispielanwendung
Polling Loop
1 class Program
2 {
3 static void Main(string[] args)
4 {
5 _looper = new Looper();
6
7 new Thread(StopLoop).Start();
8
9 _looper.WaitUntilFinished();
10 }
11
12 static void StopLoop()
13 {
14 Thread.Sleep(1000);
15 _looper.FinishLoop();
16 }
17
18 private static Looper _looper;
19 }
20
21 class Looper
22 {
23 public Looper()
24 {
25 _cancelLoop = false;
26 }
27
28 public void FinishLoop()
29 {
30 _cancelLoop = true;
31 }
32
33 public void WaitUntilFinished()
34 {
35 while (_cancelLoop == false);
36 }
37
38 private bool _cancelLoop;
39 }
Die Klasse Looper enthält einen Polling Loop. Dieser wird so lange ausgeführt bis die Loop Variable _cancelLoop gesetzt wird. Die Klasse Looper bildet somit das typische Grundgerüst für eine Polling Funktionalität wie sie in der Praxis oft anzutreffen ist.
Innerhalb der Main Funktion wird der Polling Loop gestartet und in einem parallelen Thread wird die Polling Abbruchfunktion nach einer Verzögerung von einer Sekunde ausgeführt.
Ein Test dieser Anwendung hat auf meinem Rechner zum erwarteten Ergebnis geführt. Die Konsolenanwendung wurde nach etwa einer Sekunde geschlossen. Was also spricht gegen die Verwendung der Looper Beispielklasse in einer Multithreading Anwendung? Es ist leider nicht sichergestellt, dass das Verhalten auf jedem Rechner oder auch bei jedem Start der Applikation gleich ist. Es ist möglich, dass der Wert der Variable _cancelLoop in einem Cache zwischenspeichert wird. Bei einer Änderung des Wertes der Variable in einem parallelen Thread kann es vorkommen, dass der Loop den veralteten Wert der Variablen aus dem Cache verwendet. Dadurch wird der Loop zu einem Endlos Loop und die Anwendung hängt in dieser Endlosschleife fest. In der Praxis wird dieser Fall selten auftreten, da der Cache durch andere Funktionen ebenfalls genutzt wird und somit der zwischengespeicherte Wert von _cancelLoop überschrieben und später neu geladen werden muss.
Als sorgfältiger Entwickler lehnen Sie sich aber selbstverständlich nicht zurück und ignorieren dieses Verhalten da es nur selten auftritt. Sie werden sicherlich eine Lösung bevorzugen bei der eine Endlosschleife von vornherein ausgeschlossen wird.
Diese Lösung ist mit sehr wenig Aufwand machbar. Die Variable _cancelLoop muss dazu nur als volatile gekennzeichnet werden.
38 private volatile bool _cancelLoop;
Das Schlüsselwort volatile kennzeichnet die Variable als flüchtig. Das heisst für den JIT Compiler, das er den Wert der Variablen nicht in Registern zwischenspeichern darf, sondern den Wert immer neu bestimmen muss. Somit ist auch in einer Multithreading Anwendung sichergestellt, dass der Polling Loop korrekt beendet wird.
Fazit
Die Optimierungen der Speicherverwaltung durch den Compiler, die CLR und die CPU beeinflussen das Laufzeitverhalten von Anwendungen. Dies ist bei einer Single Thread Anwendung nicht relevant, kann aber in einer Multithreading Anwendung zu unerwünschten Seiteneffekten führen. Mit Hilfe des Schlüsselwort volatile lassen sich Variablen definieren die nicht von dem gezeigten Seiteneffekt betroffen sind.