Multithreading in C#, Teil 2: Atomare Datenzugriffe

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.

Advertisements
Dieser Beitrag wurde unter .NET, C#, Multithreading abgelegt und mit verschlagwortet. Setze ein Lesezeichen auf den Permalink.

Eine Antwort zu Multithreading in C#, Teil 2: Atomare Datenzugriffe

  1. Joe Joe schreibt:

    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

Kommentar verfassen

Trage deine Daten unten ein oder klicke ein Icon um dich einzuloggen:

WordPress.com-Logo

Du kommentierst mit Deinem WordPress.com-Konto. Abmelden / Ändern )

Twitter-Bild

Du kommentierst mit Deinem Twitter-Konto. Abmelden / Ändern )

Facebook-Foto

Du kommentierst mit Deinem Facebook-Konto. Abmelden / Ändern )

Google+ Foto

Du kommentierst mit Deinem Google+-Konto. Abmelden / Ändern )

Verbinde mit %s