Multithreading in C#, Teil 11: Lock, Monitor, Mutex, Semaphore, SemaphoreSlim

In diesem Artikel möchte ich Ihnen die grundlegenden Sperrmechanismen näher bringen, mit denen sich threadsichere Quellcodebereiche realisieren lassen. Dabei werden ich Ihnen die lock, Monitor, Mutex und Semaphore Mechanismen aufzeigen.

 
lock

Mittels des lock Befehls kann sichergestellt werden, dass ein oder mehrere Code Abschnitte nicht gleichzeitig von mehreren Threads ausgeführt werden. Jeweils nur ein Thread kann dabei das Synchronisierungsobjekt sperren. Ist das Synchronisierungsobjekt gesperrt dann wartet der nächste Thread quasi vor dem lock Befehl bis das Objekt freigegeben wird. Der nachfolgende Quellcode zeigt wie sich zwei Funktionen mittels lock Befehl threadsicher realisieren lassen.

    1 class ClassWithLock

    2 {

    3     static readonly object _locker = new object();

    4 

    5     private static int _counter;

    6 

    7     public void Increase()

    8     {

    9         lock (_locker)

   10         {

   11             _counter++;

   12         }

   13     }

   14 

   15     public void Decrease()

   16     {

   17         lock (_locker)

   18         {

   19             _counter–;

   20         }

   21     }

   22 }

 
Monitor

Mittels Monitor.Enter und Monitor.Exit lässt sich ebenfalls ein Code Abschnitt durch Sperren eines Synchronisierungsobjektes threadsicher realisieren. Im Prinzip ist die Verwendung der Monitor Klasse nicht nötig, da der lock Befehl die gleiche Funktionalität abdeckt. Der lock Befehl wird dabei vom Compiler in Monitor Anweisungen umgewandelt. Daher möchte ich hier auch nicht näher auf die Monitor Klasse eingehen. Ein detaillierter Vergleich von lock und Monitor folgt im nächsten Artikel dieser Serie.

 
Nested Lock

Ein verschachtelter Aufruf des lock oder Monitor Befehls ist möglich. Dies ermöglicht es innerhalb eines gesperrten Funktionsbereiches andere Funktionen der Klasse zu nutzen, welche ebenfalls mittels lock oder Monitor gesichert sind. Das nachfolgende Beispiel zeigt wie die Funktion NestedIncrease aus der Funktion Increase heraus aufgerufen werden kann.

    1 class ClassWithNestedLock

    2 {

    3     static readonly object _locker = new object();

    4 

    5     private static int _counter;

    6 

    7     public void Increase()

    8     {

    9         lock (_locker)

   10         {

   11             NestedIncrease();

   12         }

   13     }

   14 

   15     private void NestedIncrease()

   16     {

   17         lock (_locker)

   18         {

   19             _counter++;

   20         }

   21     }

   22 }

 
Mutex

Ein Mutex verhält sich wie ein lock, erlaubt aber eine Verwendung über mehreren Prozessen hinweg. Ein Mutex kann daher in mehreren Instanzen einer Applikation oder auch in mehreren Applikationen gemeinsam genutzt werden.

Typische Anwendungsfälle für einen Mutex:

  • Anwendung nur einmalig starten
  • Inter Process Kommunikation synchronisieren
  • Synchronisation einer gemeinsamen Aufgabe zweier Prozesse
  • Synchronisation eines gemeinsamen Zugriffs auf Ressourcen aus mehreren Prozessen heraus

 

Der nachfolgende Beispielcode zeigt wie ein Mutex angelegt und verwendet werden kann.

    1 class ClassWithMutex

    2 {

    3     private static Mutex _mutex = null;       

    4 

    5     public void DoSomething()

    6     {

    7         _mutex = new Mutex(false, "MyMutex");

    8         bool lockTaken = false;

    9 

   10         try

   11         {

   12             lockTaken = _mutex.WaitOne();               

   13         }

   14         finally

   15         {

   16             if (lockTaken == true)

   17             {

   18                 _mutex.ReleaseMutex();

   19             }

   20         }           

   21     }

   22 }

 
Der Mutex wird anhand eines Namens identifiziert (hier „MyMutex“). WaitOne dient zum Sperren des Synchronisierungsobjektes. Dabei kann ein Timeout für das Warten angegeben werden. ReleaseMutex dient zum Entsperren des Synchronisierungsobjektes.

 
Semaphore

Ein Semaphore beschränkt den Zugriff auf den Code Bereich nicht nur auf einen Thread sondern die Anzahl der Threads, die gleichzeitig auf den Code Bereich zugreifen dürfen, ist definierbar. Das nachfolgende Beispiel zeigt wie ein geschützter Code Abschnitt definiert wird den maximal drei Threads gleichzeitig ausführen dürfen.

    1 class ClassWithSemaphore

    2 {

    3     private static Semaphore _semaphore = new Semaphore(3, 3);

    4 

    5     private static int _counter = 0;

    6 

    7     public void DoSomething()

    8     {

    9         _semaphore.WaitOne();

   10 

   11         _counter++;

   12         Console.WriteLine(_counter);

   13         Thread.Sleep(50);

   14         _counter–;

   15 

   16         _semaphore.Release();

   17     }

   18 }

 
Beispielhaft wird in der nachfolgenden Konsolenanwendung die Funktion DoSomething 50-mal parallel ausgeführt. Die Konsolenausgabe gibt dabei 1,2 oder 3 aus da maximal drei Threads gleichzeitig Zugriff auf den mittels Semaphore geschützten Abschnitt haben.

    1 class Program

    2 {

    3     static void Main(string[] args)

    4     {

    5         ClassWithSemaphore test = new ClassWithSemaphore();

    6 

    7         for (int i = 1; i <= 50; i++) new Thread(test.DoSomething).Start();

    8 

    9         Console.ReadKey();

   10     }

   11 }

 
SemaphoreSlim

SemaphoreSlim wurde mit .Net 4.0 eingeführt und lässt sich genauso wie Semephore anwenden. SemaphoreSlim ist schneller als Semaphore, hat aber den Nachteil dass es nicht über mehrere Prozesse hinweg verwendet werden kann.

Semaphore ist daher mit einem Monitor vergleichbar bei welchem die Anzahl gleichzeitiger Zugriffe einstellbar ist. SemaphoreSlim hingegen ist mit einem lock vergleichbar bei welchem die Anzahl gleichzeitiger Zugriffe definierbar ist.

 
Fazit

Die in diesem Artikel vorgestellten Möglichkeiten einen threadsicheren Zugriff auf Code Abschnitte zu implementieren bilden das Rückgrat jeder threadsichern Anwendung. Deren Kenntnis und das Wissen wann welche Alternative zum Einsatz kommen sollte gehört somit zum Grundwissen jedes Entwicklers der threadsicheren Quellcode schreiben möchte.

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

2 Antworten zu Multithreading in C#, Teil 11: Lock, Monitor, Mutex, Semaphore, SemaphoreSlim

  1. Gaby schreibt:

    Windows has the CRITICAL_SECTION, which sounds like it is the same as the Linux futex. A CRITICAL_SECTION tries doing the test-and-set in user space. If that doesn’t work then it can be told to spin for a while (on multi-core symtses). If that doesn’t work then it uses a kernel mutex to wait. Thus, it gives great performance, without ending up in a long spin loop.I’ve seen many people try to implement their own mutexes. They all handle contention poorly. Either they yield under contention with Sleep(), meaning that they stay idle too long, or they spin in a busy loop for hundreds or thousands of milliseconds, wasting power and causing priority inversions. Some of them even omit memory barriers so that they are inefficient *and* incorrect.Use the operating system primitives. Set a spin count on a CRITICAL_SECTION and use it rather than rolling your own. Please.

  2. Johannes schreibt:

    Bei SemaphoreSlim: Müsste wohl eher heissen, dass Semaphore mit einem Mutex vergleichbar ist. Monitor und lock wäre ja dasselbe. Gruss und danke für den Artikel.

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 )

Facebook-Foto

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

Verbinde mit %s