1. Einführung
Erinnern wir uns an das Beispiel aus der vorherigen Vorlesung: zwei Threads in unserer noch einfachen Anwendung inkrementieren einen gemeinsamen Zähler, aber das Endergebnis stimmt nicht immer mit dem Erwarteten überein. Zum Schutz dieses Zählers haben wir bereits das keyword lock (oder genauer Monitor) verwendet, das zur Synchronisation nur innerhalb eines Prozesses passt. Aber was, wenn dein Programm nicht das einzige ist, das die Ressource benutzen will? Zum Beispiel schreibst du einen Service, der in zwei Instanzen läuft, und beide wollen in dieselbe Datei schreiben… oder ein Gerät wie einen Druckerport verwalten? Dann kommt der gute alte Mutex (von mutual exclusion — „gegenseitiger Ausschluss“) zur Hilfe.
Konzept
Ein Mutex ist ein Synchronisationsprimitiv, das den Zugriff nicht nur zwischen Threads innerhalb eines Prozesses beschränkt, sondern auch erlaubt, den Zugriff zwischen verschiedenen Prozessen auf derselben Maschine zu koordinieren. Stell ihn dir wie ein großes „Besetzt“-Schild vor der Besprechungsraumtür vor, das sowohl Mitarbeiter als auch Besucher sehen.
In .NET ist dafür die Klasse System.Threading.Mutex vorgesehen.
Wann Mutex wirklich nötig ist:
- Wenn du den Zugriff zwischen Threads in verschiedenen Prozessen synchronisierst (z.B. zwei separate Anwendungen arbeiten an derselben Datei).
- Wenn die Ressource so wertvoll und unteilbar ist, dass selbst der Prozesskontext die Zugriffsrechte nicht teilen kann.
Zur Synchronisation nur zwischen Threads eines Prozesses verwendet man normalerweise lock (Monitor). Mutex sollte für Interprozess-Synchronisation verwendet werden, weil er „schwerer“ und langsamer ist.
Wie Mutex funktioniert
flowchart TD
A(Prozess 1) --|Anfordert|--> M(Mutex)
B(Prozess 2) --|Anfordert|--> M
M --|Nur einer bekommt Erlaubnis|--> R(Geteilte Ressource)
A --|Gibt frei|--> M
B --|Nach Freigabe|--> M
2. Grundsyntax der Arbeit mit Mutex
Erzeugung
Ein Mutex wird genauso einfach erzeugt wie die meisten anderen Synchronisationsklassen:
using System.Threading;
Mutex mutex = new Mutex();
Wichtige Methoden
- WaitOne() — Versuch, den Mutex zu übernehmen; wenn es nicht klappt, wird der Thread blockiert, bis der Mutex von jemand anderem freigegeben wird.
- ReleaseMutex() — gibt den Mutex frei und erlaubt anderen Threads oder Prozessen, in die kritische Sektion zu gelangen.
Einfachstes Beispiel: Synchronisation zwischen Threads innerhalb eines Prozesses
using System;
using System.Threading;
class Program
{
static Mutex mutex = new Mutex();
static void Main()
{
Thread t1 = new Thread(PrintNumbers);
Thread t2 = new Thread(PrintNumbers);
t1.Start();
t2.Start();
t1.Join();
t2.Join();
}
static void PrintNumbers()
{
for (int i = 0; i < 5; i++)
{
mutex.WaitOne(); // In die kritische Sektion eintreten
Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId}: {i}");
mutex.ReleaseMutex(); // Kritische Sektion verlassen
Thread.Sleep(100); // Zur Veranschaulichung
}
}
}
In diesem Beispiel bekommen beide Threads abwechselnd Zugriff auf die Konsole zum Ausgeben.
3. Interprozess-Synchronisation mit benanntem Mutex
Für die „schwere Artillerie“ — Synchronisation zwischen Prozessen — verwendet man einen benannten Mutex. Du gibst ihm einen Namen, und alle Prozesse auf dem Rechner können darauf zugreifen.
Mutex mutex = new Mutex(false, "MyApp_Mutex");
Parameter des Konstruktors:
- Erster Parameter (bool initiallyOwned) — ob der Thread den Mutex sofort nach der Erstellung übernehmen will. Normalerweise — false.
- Der zweite Parameter — der Name des Mutex. Alle Prozesse, die einen Mutex mit demselben Namen benutzen, greifen auf dasselbe Objekt zu, z.B. "MyApp_Mutex".
Beispiel: zwei Anwendungen mit demselben Mutex
Starte das gleiche Programm in zwei verschiedenen Fenstern, um den Effekt zu sehen.
using System;
using System.Threading;
class Program
{
static void Main()
{
using (Mutex mutex = new Mutex(false, "MySuperUniqueMutexName"))
{
Console.WriteLine("Versuche, die kritische Sektion zu betreten...");
mutex.WaitOne(); // Warten, bis ein anderer Prozess den Mutex freigibt
try
{
Console.WriteLine("Die kritische Sektion ist von diesem Prozess belegt.");
Console.WriteLine("Drücke Enter, um die kritische Sektion zu verlassen.");
Console.ReadLine();
}
finally
{
mutex.ReleaseMutex();
Console.WriteLine("Die kritische Sektion wurde freigegeben.");
}
}
}
}
Probier es aus:
- Öffne zwei Fenster mit dieser Anwendung.
- Starte beide — der zweite wird warten, bis du in dem ersten Enter drückst.
4. Gleichzeitiger Start der Anwendung
Mutex wird oft verwendet, um die Anzahl gleichzeitiger gestarteter Instanzen zu begrenzen. Beispiel: „Hey, Benutzer, öffne nicht zwei Kopien des Rechners gleichzeitg!“
using System;
using System.Threading;
class Program
{
static void Main()
{
bool createdNew;
using (Mutex mutex = new Mutex(true, "CalculatorAppInstanceMutex", out createdNew))
{
if (!createdNew)
{
Console.WriteLine("Die Anwendung läuft bereits!");
return;
}
Console.WriteLine("Die Anwendung wurde erfolgreich gestartet. Drücke Enter zum Beenden.");
Console.ReadLine();
}
}
}
Dieses Pattern sieht man oft in Desktop-Anwendungen: die erste Instanz startet, die zweite meldet nur, dass sie schon läuft, und beendet sich. Das offizielle Microsoft-Beispiel steht in der Dokumentation.
5. Typische Fehler bei der Arbeit mit Mutex
Fehler Nr.1: ReleaseMutex() vergessen aufzurufen.
Wenn ein Thread den Mutex erfolgreich übernommen hat (WaitOne()), aber ReleaseMutex() nicht aufruft (wegen einer Ausnahme oder schlicht vergessen), kann kein anderer Thread oder Prozess hinein, bis der „vergessliche“ Thread endet. Das kann zu einem deadlock führen. Guter Stil ist immer try-finally zu verwenden:
mutex.WaitOne();
try
{
// kritische Sektion
}
finally
{
mutex.ReleaseMutex();
}
Fehler Nr.2: falsche Anzahl von ReleaseMutex()-Aufrufen.
Wenn du ReleaseMutex() öfter aufrufst, als WaitOne() verwendet wurde, wird eine ApplicationException geworfen.
Fehler Nr.3: Versuch, einen nicht eigenen Mutex freizugeben.
Ein Mutex ist an den Thread gebunden, der ihn übernommen hat. Nur dieser Thread darf ReleaseMutex() aufrufen. Wenn ein anderer Thread versucht, ihn freizugeben, wirft .NET eine Ausnahme.
Fehler Nr.4: Mutex benutzen, wo ein lock ausreicht.
Mutex ist langsamer als normales lock, weil er zwischen Prozessen arbeiten kann und Systemaufrufe benötigt. Empfehlung: wenn keine Interprozess-Synchronisation nötig ist — benutze lock.
Fehler Nr.5: unpassender Mutex-Name.
Wenn du einen zu einfachen Namen wählst (z.B. "MyMutex"), kannst du versehentlich mit einer Fremdsoftware „konfligieren“, die denselben Namen benutzt. Besser sind eindeutige Namen (z.B. mit Firmennamen oder App-Identifikator).
6. Nützliche Feinheiten
WaitOne(timeout)
Man kann ein Timeout für das Warten auf den Mutex angeben:
if (mutex.WaitOne(5000)) // bis zu 5 Sekunden warten
{
try { /* ... */ }
finally { mutex.ReleaseMutex(); }
}
else
{
Console.WriteLine("Konnte in 5 Sekunden keinen Zugriff auf die Ressource bekommen!");
}
Mutex.TryOpenExisting
Wenn du dich an einen bereits existierenden Mutex anhängen willst, benutze die statische Methode:
if (Mutex.TryOpenExisting("DiaryFileWriteMutex", out Mutex existingMutex))
{
// jetzt ist existingMutex eine Referenz auf den existierenden Mutex
}
Trennung zwischen Benutzern
Standardmäßig ist ein benannter Mutex für alle Benutzer verfügbar, die Rechte zum Erzeugen von Systemobjekten haben. Wenn strengere Kontrolle nötig ist — benutze den Konstruktor mit MutexSecurity.
Vergleichstabelle: lock/Monitor, Mutex, Semaphore
| Primitiv | Interprozess-Synchronisation? | Geschwindigkeit | Wo einsetzen |
|---|---|---|---|
|
Nein | Sehr hoch | Zwischen Threads in einem Prozess |
|
Ja | Niedriger (teurer) | Zwischen Threads verschiedener Prozesse |
|
Ja (bei NamedSemaphore) | Vergleichbar mit Mutex | Wenn die Anzahl der Threads begrenzt werden muss |
Mehr zu Semaphoren — in der nächsten Vorlesung.
Zustände von Mutex
stateDiagram-v2
[*] --> Unowned
Unowned --> Owned : WaitOne()
Owned --> Owned : WaitOne() (reentrant)
Owned --> Unowned : ReleaseMutex()
Owned --> Abandoned : Thread "stirbt", ohne ReleaseMutex() aufzurufen
Abandoned --> Unowned
- Unowned — niemand besitzt den Mutex.
- Owned — der Mutex ist von einem Thread übernommen.
- Abandoned — ein Thread ist plötzlich beendet, ohne ReleaseMutex() aufzurufen. Der nächste, der den Mutex bekommt, erhält eine AbandonedMutexException (das ist ein Hinweis auf einen Fehler und einen potentiell inkonsistenten Zustand der Ressource).
GO TO FULL VERSION