Wikipedia · einfach zusammengefasst · Stand
Mutex
Ein kritischer Abschnitt (engl. critical section oder critical region) ist derjenige Teil im ausführbaren Code, in dem ein wegen des Mutex ungestörter …
Inhalt6 Abschnitte
Grundidee und Zweck
Ein Mutex, kurz für englisch mutual exclusion, bedeutet wechselseitiger Ausschluss. Gemeint ist eine Gruppe von Verfahren, die das Problem des kritischen Abschnitts lösen. Ein kritischer Abschnitt ist der Teil eines Programms, in dem ein Prozess oder Thread auf gemeinsam genutzte Daten zugreift und diese verändern kann. Ohne Schutz könnten mehrere nebenläufige Prozesse oder Threads dieselben Daten gleichzeitig oder zeitlich verschränkt verändern. Dadurch kann eine Datenstruktur inkonsistent werden, obwohl jede einzelne Aktion für sich genommen korrekt wäre.
Mutex-Verfahren koordinieren den zeitlichen Ablauf nebenläufiger Prozesse oder Threads so, dass immer nur ein Prozess oder Thread einen geschützten kritischen Abschnitt ausführt. Solange sich ein Prozess oder Thread im kritischen Abschnitt befindet, werden andere vom Zugriff ausgeschlossen. Das ist besonders wichtig in Systemen mit gemeinsam genutzten Daten, zum Beispiel in Client/Server-Systemen, in denen unabhängige Client-Prozesse oder -Threads gleichzeitig auf einen Datenbank-Server zugreifen.
Mutex gehört zur Prozess- oder Thread-Synchronisation. Prozesssynchronisation bedeutet allgemein, den zeitlichen Ablauf mehrerer nebenläufiger Prozesse oder Threads zu koordinieren. Beim Mutex ist diese Synchronisation meist nur für kurze Zeit nötig: nämlich genau während des geschützten Zugriffs.
Begriffe und Abgrenzung
Der Begriff Mutex wird nicht einheitlich verwendet. Er kann entweder das Verfahren zum Sicherstellen des gegenseitigen Ausschlusses meinen oder ein Objekt beziehungsweise eine Datenstruktur, die diesen Ausschluss erzwingt. Viele Programmiersprachen mit Unterstützung für Nebenläufigkeit kennen solche Mutex-Objekte.
Ein Mutex ist nicht einfach dasselbe wie ein binäres Semaphor. Ein Semaphor ist eine Datenstruktur zur Steuerung eines ausschließenden Zugriffs auf Daten, mit oder ohne Verbindung zu einem Task-Scheduler. Semaphore dürfen von anderen Aktivitätsträgern freigegeben werden, was sie vom Mutex unterscheidet.
Ein Monitor ist ein Mechanismus zur Steuerung eines ausschließenden Zugriffs auf Daten in Verbindung mit der Rechenzeitverteilung, also dem Scheduler, eines Echtzeitbetriebssystems oder einer Programmiersprache mit Multithreading-Unterstützung, zum Beispiel Java. Semaphor- und Monitor-Technik sind konzeptionell verwandt.
Lock-Mechanismen dienen allgemein dazu, den Zugriff von anderer Seite während einer laufenden Bearbeitung zu sperren. Beispiele sind Dateisperren beim Schreiben in eine Datei oder das Sperren einer bestimmten Revision in einem Versionsverwaltungssystem.
Ein kritischer Abschnitt darf nicht von einem anderen Thread unterbrochen werden, der auf dieselben durch Mutex geschützten Daten zugreifen will. Er soll auch nicht von einem anderen Thread so unterbrochen werden, dass der Mutex-Zugriff unzulässig verlängert wird.
Umsetzung mit Semaphor, Monitor und Lock
Zur Realisierung eines Mutex wird einem Datenobjekt ein Kontrollelement zugeordnet. Jeder Prozess oder Thread muss dieses Kontrollelement beachten, bevor er Programmcode ausführt, der auf das Datenobjekt zugreift. Ist ein anderer Prozess oder Thread bereits im kritischen Abschnitt, muss der neue Zugriff warten.
Dabei soll verhindert werden, dass eine Bearbeitung beginnt, während ein anderer Thread die Daten bearbeitet oder auch nur konsistent liest. Außerdem darf eine Bearbeitung nicht so unterbrochen werden, dass ein anderer Thread konkurrierende Änderungen vornimmt, die zu Inkonsistenzen führen. Auch darf kein anderer Prozess während der Bearbeitung zwischenzeitlich inkonsistente Daten verwenden.
Bei einer Semaphor-Lösung zeigt ein Thread über das Semaphor an, dass er mit der Bearbeitung beginnt. Vorher wird geprüft, ob das Semaphor frei ist. Ist es belegt, muss der Thread warten. Er kann das Semaphor zyklisch per Polling abfragen oder seine Thread-Identifizierung in einer Warteschlange am Semaphor hinterlegen und in den Wartezustand gehen. Ist das Semaphor frei, wird es belegt. Nach Ende der Datenbearbeitung muss es wieder freigegeben werden.
In einem Echtzeitbetriebssystem kann der Scheduler die Freigabe unterstützen. Er prüft, ob andere Threads auf das Semaphor warten, setzt sie auf „bereit“ (engl. readyToRun) und arbeitet sie entsprechend ihrer Priorität ab.
In Java gibt es mit synchronized eine Standardlösung. Ein Block wie synchronized(obj) { ... } ist ein kritischer Abschnitt. Andere Threads, die ebenfalls synchronized(obj) aufrufen, müssen warten, bis der Abschnitt beendet ist. In der Java-Dokumentation wird dieses Konzept als Monitor bezeichnet. Auch eine ganze Methode kann als synchronized gekennzeichnet werden; dann gilt die gesamte Methode als kritischer Abschnitt.
Ein Lock-Mechanismus ähnelt Semaphor und Monitor, ist aber besonders auf mehrere Prozesse in verteilten Systemen abgestimmt. Mit Read-Write-Locks lässt sich das Konzept verfeinern: Prozesse, die nur lesen, behindern sich gegenseitig nicht. Das ist besonders beim Zugriff auf Dateien und Datenbanken verbreitet.
Warten auf den Zugriff
Wenn ein Mutex aktiv ist, darf ein anderer Prozess oder Thread nicht auf den geschützten Bereich zugreifen. Er hat grundsätzlich drei Möglichkeiten: Er kann nur warten, er kann währenddessen andere Aufgaben ausführen, oder er kann den Zugriff verwerfen.
Beim passiven Warten wird die Kontrolle über den wartenden Thread an einen Scheduler abgegeben. Der Thread wird erst fortgesetzt, wenn der Mutex frei ist. Das setzt voraus, dass der Thread, der den Mutex belegt, im selben Scheduler eingebunden ist und dass der Scheduler den Mutex und seine Freigabe erkennt. Das ist häufig bei mehreren Threads eines Prozesses der Fall und kann auch bei mehreren Prozessen unter einem gemeinsamen Betriebssystem-Kernel umgesetzt sein.
Aktives Warten, englisch busy waiting, bedeutet fortwährendes Abfragen, ob der Mutex frei ist. Diese fortwährende Abfrage heißt Polling. Ein Spinlock ist die Kombination von Lock und Polling. Aktives Warten kann nötig sein, wenn Prozesse keine Verbindung über einen gemeinsamen Scheduler haben oder wenn ein Thread neben dem Warten noch andere Aufgaben erfüllen muss. Ein Beispiel im Artikel ist ein hochpriorisierter Thread, der zyklisch eine Regelung ausführen muss und zusätzlich Messwerte an einen anderen Thread übergeben soll. Wenn der Mutex gerade belegt ist, darf er die Werte nicht ablegen, muss aber seine Regelungsaufgaben fortsetzen und später erneut prüfen.
Beim aktiven Warten soll eine zu häufige Abfrage des Mutex-Steuerelements vermieden werden. Sinnvoll kann ein wait(millisekunden)- oder sleep-Aufruf sein, damit Rechenzeit abgegeben oder Strom gespart wird.
Das Verwerfen des Zugriffs wird meist dann verwendet, wenn ein späterer Wert den ursprünglichen Eintrag ohnehin überschreiben würde. Dann kann der aktuell nicht schreibbare Wert sofort verworfen werden.
Unterstützung und Tests
Einige Programmiersprachen unterstützen Mutex direkt als Teil der Sprache, besonders Concurrent Pascal, Ada, Java und C#. Für fast alle Sprachen gibt es Bibliotheken, die ein Mutex-System implementieren, zum Beispiel pthreads in C. Häufig gehören solche Funktionen zur Standard-API oder zur Laufzeitumgebung.
Eine gute Mutex-Implementierung ist nur mit einem Betriebssystem möglich, dessen Scheduler solche Konzepte unterstützt. Auf anderen Systemen, insbesondere Echtzeitsystemen, muss auf Spinlocks zurückgegriffen werden. Diese können die Systemleistung durch Busy Waiting erheblich beeinträchtigen. Grundsätzlich genügt es, wenn ein Betriebssystem oder eine Laufzeitumgebung ein Subset aus Mutex, Semaphor, Monitor, Lock oder Critical Section anbietet, weil jedes dieser Prinzipien durch ein anderes aus der Gruppe modelliert werden kann.
Beim Testen von Multithread-Anwendungen gelten die klassischen Testmethoden Modultest, Codereview und Praxistest weiterhin, aber mit Besonderheiten. Im Praxistest treten Fehler durch Multithreading unter normalen Betriebsbedingungen möglicherweise gar nicht auf. Die Aussage „Test fehlerfrei, also Software fehlerfrei“ ist deshalb nicht schlüssig. Fehler können erst durch veränderte Bedingungen sichtbar werden, etwa durch Timingverschiebungen oder geänderte Ressourcennutzung.
Im Modultest kann man gezielt Multithread-Bedingungen einbauen, indem an bekannten kritischen Stellen ein Threadwechsel erzwungen wird. In C oder C++ kann dafür ein Makro wie TEST_Threadswitch() verwendet werden, das im Produktionscode leer definiert wird. In Java kann man ähnliche Teststimuli über Interface-Methoden einbauen und im Praxiseinsatz durch leere Implementierungen ersetzen.
Beim Codereview wird systematisch geprüft, ob alle Zugriffe auf bestimmte Daten mit einem Mutex geschützt sind. So kann auffallen, wenn ein Schutz an einer Stelle vergessen wurde. Hilfsprogramme wie lint können in C oder C++ dabei unterstützen. In Java kann zwar ein synchronized-Schlüsselwort vergessen werden, eine als synchronized deklarierte Methode ist aber automatisch bezüglich der eigenen Klasse thread-sicher. Das ist jedoch kein Allheilmittel, weil synchronized-Methoden möglicherweise zu viel blockieren.
Typische Probleme
Mutex kann zu Verklemmungen führen, sogenannten Deadlocks. Dabei kann keiner der beteiligten Prozesse fortfahren, weil sie sich gegenseitig blockieren. Ein anschauliches Beispiel dafür ist das Philosophenproblem. Solche Verklemmungen können durch geeignete Planung des Programmablaufs vermieden werden, zum Beispiel nach dem Peterson-Algorithmus oder dem Dekker-Algorithmus.
Ein weiteres Problem ist, dass Systeme das Verhalten bei mehrfachem, also rekursivem, Aufruf eines Mutex-Locks aus demselben Thread unterschiedlich definieren oder implementieren. Einige Systeme verwenden dafür einen Zähler. Andere blockieren den Thread ähnlich wie bei einer Semaphore, geben eine Fehlermeldung zurück oder erlauben, das Verhalten einzustellen.
Außerdem gibt es die Prioritätsinversion. Sie kann auftreten, wenn mindestens drei Threads unterschiedliche Prioritäten haben. Dann kann ein Thread mittlerer Priorität indirekt den Thread mit höchster Priorität blockieren, während der Thread mit niedrigster Priorität gerade Zugriff auf eine Ressource hat.