Im vorhergehenden Artikel dieser Artikelserie wurde gezeigt wie sich atomar ausführbare Funktionen mittels Lock Mechanismus oder Interlocked Klasse realisieren lassen. Neben Funktionsaufrufen sind Datenzugriffe ein weiterer wichtiger Aspekt bei der Programmierung. Daher möchte ich Ihnen in diesem Artikel aufzeigen was bei parallelen Datenzugriffen zu beachten ist.
Das nachfolgende Beispiel zeigt eine Anwendung bei der eine GUID Variable abwechselnd mit zwei Werten gefüllt wird. In einem parallelen Thread wird die Variable gelesen. Dabei wird gezählt wie oft die beiden Werte auftreten. Zusätzlich wird der Fall betrachtet, dass der gelesene Wert keinem der beiden erwarteten Werte entspricht.
1. GUID Variable lesen und schreiben ohne lock
1 class Program
2 {
3 static void Main(string[] args)
4 {
5 _countValueA = 0;
6 _countValueB = 0;
7
8 _guid = new Guid(1, 1, 1, new byte[] { 1, 1, 1, 1, 1, 1, 1, 1 });
9
10 Parallel.Invoke(SetValue, GetValue);
11
12 Console.WriteLine(„ValueA: „ + _countValueA);
13 Console.WriteLine(„ValueB: „ + _countValueB);
14 Console.WriteLine(„Total: „ + (_countValueA + _countValueB));
15 Console.Read();
16 }
17
18 private static void SetValue()
19 {
20 for (int i = 0; i < 1000000; i++)
21 {
22 if (i % 2 == 0)
23 {
24 _guid = new Guid(1, 1, 1, new byte[] { 1, 1, 1, 1, 1, 1, 1, 1 });
25 }
26 else
27 {
28 _guid = new Guid(2, 2, 2, new byte[] { 2, 2, 2, 2, 2, 2, 2, 2 });
29 }
30 }
31 }
32
33 private static void GetValue()
34 {
35 string actualValue;
36
37 for (int i = 0; i < 1000000; i++)
38 {
39 actualValue = _guid.ToString();
40
41 if (actualValue.Equals(„00000001-0001-0001-0101-010101010101“))
42 {
43 Interlocked.Increment(ref _countValueA);
44 }
45 else if (actualValue.Equals(„00000002-0002-0002-0202-020202020202“))
46 {
47 Interlocked.Increment(ref _countValueB);
48 }
49 else
50 {
51 Console.WriteLine(actualValue);
52 }
53 }
54 }
55
56 private static Guid _guid;
57
58 private static int _countValueA;
59 private static int _countValueB;
60 }
Auch bei Variablenzugriffen stellt sich die Frage ob der Zugriff atomar erfolgt. Die GUID wird abwechselnd mit „1“ oder „2“ gefüllt. Somit werden beim Auslesen der Variable diese beiden Werte erwartet. Das Konsolenprogramm generiert aber folgende Ausgabe:
00000002-0002-0002-0101-010101010101
00000001-0001-0001-0101-010102020202
…
00000001-0002-0002-0202-020202020202
00000002-0002-0001-0101-010101010101
ValueA: 12513
ValueB: 984114
Total: 996627
Die GUID wird also nicht nur abwechselnd mit „1“ oder „2“ gefüllt sondern teilwiese sind Werte vorhanden welche teilweise „1“ und teilweise „2“ enthalten. Sie können sich sicher schon denken warum dies der Fall ist. Ja, auch die Variablenzugriffe erfolgen nicht atomar. Der Wert des komplexen Datentyp GUID kann nicht in einem atomaren Schritt geschrieben oder gelesen werden. Daher kann es vorkommen das beide Threads quasi gleichzeitig auf die Variable zugreifen und jeweils Teile der GUID verändern.
Beheben lässt sich dieser Konflikt wiederum durch den Einsatz der lock Funktion. Das nachfolgende Beispiel zeigt den Quellcode, erweitert um die lock Funktion.
2. GUID Variable lesen und schreiben mit lock
1 class Program
2 {
3 static void Main(string[] args)
4 {
5 _countValueA = 0;
6 _countValueB = 0;
7
8 _guid = new Guid(1, 1, 1, new byte[] { 1, 1, 1, 1, 1, 1, 1, 1 });
9
10 Parallel.Invoke(SetValue, GetValue);
11
12 Console.WriteLine(„ValueA: „ + _countValueA);
13 Console.WriteLine(„ValueB: „ + _countValueB);
14 Console.WriteLine(„Total: „ + (_countValueA + _countValueB));
15 Console.Read();
16 }
17
18 private static void SetValue()
19 {
20 for (int i = 0; i < 1000000; i++)
21 {
22 lock (_lock)
23 {
24 if (i % 2 == 0)
25 {
26 _guid = new Guid(1, 1, 1, new byte[] { 1, 1, 1, 1, 1, 1, 1, 1 });
27 }
28 else
29 {
30 _guid = new Guid(2, 2, 2, new byte[] { 2, 2, 2, 2, 2, 2, 2, 2 });
31 }
32 }
33 }
34 }
35
36 private static void GetValue()
37 {
38 string actualValue;
39
40 for (int i = 0; i < 1000000; i++)
41 {
42 lock (_lock)
43 {
44 actualValue = _guid.ToString();
45 }
46
47 if (actualValue.Equals(„00000001-0001-0001-0101-010101010101“))
48 {
49 Interlocked.Increment(ref _countValueA);
50 }
51 else if (actualValue.Equals(„00000002-0002-0002-0202-020202020202“))
52 {
53 Interlocked.Increment(ref _countValueB);
54 }
55 else
56 {
57 Console.WriteLine(actualValue);
58 }
59 }
60 }
61
62 private static Guid _guid;
63
64 private static int _countValueA;
65 private static int _countValueB;
66
67 private static object _lock = new object();
68 }
Das Konsolenprogramm generiert nun folgende Ausgabe:
ValueA: 178568
ValueB: 821432
Total: 1000000
Der Einsatz der lock Funktion hat also zum gewünschten Resultat geführt.
Eine GUID ist ein relativ komplexer Datentyp. Was aber ist wenn ein einfacher Datentyp verwendet wird? Sind dann auch lock Mechanismen nötig oder erfolgt der Zugriff auf einfache Datentypen atomar? Zur Beantwortung dieser Fragen dient die nachfolgende Konsolenanwendung. In dieser Anwendung wurde die GUID Variable durch eine Int64 Variable ersetzt. Der Wert der Variablen wird ebenfalls abwechselnd mit zwei Werten überschrieben und in einem parallelen Thread gelesen.
3. Int64 Variable lesen und schreiben ohne lock
1 class Program
2 {
3 static void Main(string[] args)
4 {
5 _countValueA = 0;
6 _countValueB = 0;
7
8 _value = 1000000000000000001;
9
10 Parallel.Invoke(SetValue, GetValue);
11
12 Console.WriteLine(„ValueA: „ + _countValueA);
13 Console.WriteLine(„ValueB: „ + _countValueB);
14 Console.WriteLine(„Total: „ + (_countValueA + _countValueB));
15 Console.Read();
16 }
17
18 private static void SetValue()
19 {
20 for (int i = 0; i < 1000000; i++)
21 {
22 if (i % 2 == 0)
23 {
24 _value = 1000000000000000001;
25 }
26 else
27 {
28 _value = 4000000000000000004;
29 }
30 }
31 }
32
33 private static void GetValue()
34 {
35 string actualValue;
36
37 for (int i = 0; i < 1000000; i++)
38 {
39 actualValue = _value.ToString();
40
41 if (actualValue.Equals(„1000000000000000001“))
42 {
43 Interlocked.Increment(ref _countValueA);
44 }
45 else if (actualValue.Equals(„4000000000000000004“))
46 {
47 Interlocked.Increment(ref _countValueB);
48 }
49 else
50 {
51 Console.WriteLine(actualValue);
52 }
53 }
54 }
55
56 private static Int64 _value;
57
58 private static int _countValueA;
59 private static int _countValueB;
60 }
Die Anwendung verwendet keine Lock Funktion. Daher sind die Variablenzugriffe nicht gegen parallele Zugriffe geschützt. Folgende Szenarien sind bei der Ausführung der Anwendung denkbar:
- Der Zugriff auf die Variable ist nicht atomar. Genau wie die GUID Variable wird auch die Int64 Variable neben den beiden erwarteten Werten auch unerwartete Werte annehmen.
- Der Zugriff auf die Variable erfolgt atomar. Somit wird die Variable nur die zwei erwarteten Werte annehmen.
Was denken Sie? Welches der beiden Szenarien wird eintreten? Die richtige Antwort lautet: Es kommt darauf an! Die Int64 Variable ist 64bit gross. Daher ergeben sich Unterschiede je nachdem ob die Anwendung in einer 32bit oder 64bit Umgebung ausgeführt wird.
Das Konsolenprogramm generiert folgende Ausgabe wenn es auf einem 64bit Rechner und als 64bit Anwendung ausgeführt wird:
ValueA: 14764
ValueB: 985236
Total: 1000000
Das gleiche Konsolenprogramm, diesmal aber als 32bit Anwendung kompiliert, erzeugt folgende Ausgabe:
999999999835111428
…
4000000000164888577
ValueA: 103
ValueB: 99870
Total: 999973
In der 32bit Anwendung erfolgt der Variablenzugriff nicht atomar. Dies ist logisch, da die Variable 64bit gross ist und somit in zwei Schritten geschrieben werden muss. Daraus resultieren die nicht erwarteten Variablenwerte. Diese bestehen aus einer Kombination der ersten 32 Bit eines Wertes und der zweiten 32 Bit des zweiten Wertes. Der Zugriff auf vermeintlich einfache Datentypen muss daher ebenfalls als nicht atomar angesehen werden und durch lock Mechanismen geschützt werden. Der folgende Quellcode zeigt das Beispiel erweitert um die entsprechenden lock Funktionen.
4. Int64 Variable lesen und schreiben mit lock
1 class Program
2 {
3 static void Main(string[] args)
4 {
5 _countValueA = 0;
6 _countValueB = 0;
7
8 _value = 1000000000000000001;
9
10 Parallel.Invoke(SetValue, GetValue);
11
12 Console.WriteLine(„ValueA: „ + _countValueA);
13 Console.WriteLine(„ValueB: „ + _countValueB);
14 Console.WriteLine(„Total: „ + (_countValueA + _countValueB));
15 Console.Read();
16 }
17
18 private static void SetValue()
19 {
20 for (int i = 0; i < 1000000; i++)
21 {
22 lock (_lock)
23 {
24 if (i % 2 == 0)
25 {
26 _value = 1000000000000000001;
27 }
28 else
29 {
30 _value = 4000000000000000004;
31 }
32 }
33 }
34 }
35
36 private static void GetValue()
37 {
38 string actualValue;
39
40 for (int i = 0; i < 1000000; i++)
41 {
42 lock (_lock)
43 {
44 actualValue = _value.ToString();
45 }
46
47 if (actualValue.Equals(„1000000000000000001“))
48 {
49 Interlocked.Increment(ref _countValueA);
50 }
51 else if (actualValue.Equals(„4000000000000000004“))
52 {
53 Interlocked.Increment(ref _countValueB);
54 }
55 else
56 {
57 Console.WriteLine(actualValue);
58 }
59 }
60 }
61
62 private static Int64 _value;
63
64 private static int _countValueA;
65 private static int _countValueB;
66
67 private static object _lock = new object();
68 }
Durch die lock Funktion erfolgt der Variablenzugriff atomar und generiert die erwarteten Ergebnisse.
Ergänzend sollten Sie beachten, dass es einfache Datentypen gibt, bei welchen immer ein atomarer Zugriff durch das .NET Framework gewährleistet wird. Dies sind die folgenden Datentypen: bool, char, byte, sbyte, short, ushort, uint, int und float. Bei diesen Datentypen kann der lock Mechanismus entfallen.
Fazit
Variablenzugriffe erfolgen bis auf wenige Ausnahmen nicht atomar. In einer Multithreadinganwendung müssen daher die gemeinsamen Variablenzugriffe der einzelnen Threads synchronisiert werden. Dazu bietet sich die lock Funktion an. Mit dieser lassen sich Codeabschnitte als atomar ausführbar definieren.
2. GUID Variable lesen und schreiben mit lock — Quellcode habe ich zu viel ausgeführt und Konsolenprogramm generiert manchmal folgende Ausgabe, Warum?
00000000-0000-0000-0000-000000000000
00000000-0000-0000-0000-000000000000
00000000-0000-0000-0000-000000000000
00000000-0000-0000-0000-000000000000
00000000-0000-0000-0000-000000000000
ValueA: 447873
ValueB: 552122
Total: 999995
I have noticed you don’t monetize your website, don’t waste your traffic, you can earn extra cash every
month because you’ve got hi quality content.
If you want to know how to make extra money, search for: Mertiso’s tips best adsense alternative
Super Artikelreihe …
eine Frage aber zu diesem.
Das verwenden der Interlocked.Increment – Funktion sollte in den obigen Beispielen nicht notwendig seit, da via Invoke ja nur ein „GetValue“-Thread gestartet wird.
… Oder liege ich da falsch
Gruß Erwin