Prozesssynchronisation
von Florian Rittmeier
Inhalt
In einigen Situationen ist es notwendig, dass ein Prozess auf die Erledigung einer Aufgabe eines anderen Prozesses wartet. Wenn es sich hierbei um zwei eigenständige Anwendungen handelt, so bietet Visual Basic von Haus aus keine Möglichkeit, die Arbeit beider Anwendungen zu koordinieren.
Da dies jedoch eine Aufgabe ist, die auch in anderen Gebieten, z.B. der Thread-Synchronisation, auftaucht und dort sogar von immenser Wichtigkeit ist, bietet die Windows API zur Lösung dieser Aufgabe einige Möglichkeiten. Dabei handelt es sich um eine spezielle Sammlung von Funktionen, den "Synchronization Functions".
Diese werden prinzipiell zur schon genannten Thread-Synchronisation, beim Zugriff auf gemeinsame Ressourcen, verwendet.
Die folgenden Erläuterung arbeiten zum Großteil auf Threadebene. Später wird dann erklärt, warum man dies trotzdem zur Prozesssynchronisation verwenden kann.
SendMessage
Der Ein oder Andere mag nun sagen, dass er die Synchronisation auch über das Senden von Nachrichten via SendMessage erledigen könne. Jedoch muss man hierzu zunächst die Nachrichtenqueue subclassen und hierdurch gibt es Geschwindigkeitseinbußen.
Mutex
Der Weg den API-Funktionen zur Synchronisation bieten, kann in gewisser Weise wesentlich eleganter verwendet werden, da man alle notwendigen API-Funktionen bequem in einer Klasse kapseln kann.
Zunächst sollte ich klären was ein Mutex ist. Wie schon oben erwähnt, bieten diese API-Funktionen Möglichkeiten, den Zugriff auf gemeinsame Ressourcen zu regeln. Dabei geht es darum, dass immer nur ein Thread auf die gemeinsame Ressource zu einem Zeitpunkt zugreifen kann. Während dieser Zeit wird den anderen Threads der Zugriff verwehrt. Dies wird dadurch erreicht, dass wenn ein Thread einen sogenannten kritischen Abschnitt betreten will, er eine Funktion aufruft, welche den Thread solange schlafen lässt, bis er den kritischen Abschnitt betreten darf. Beim Verlassen des kritischen Abschnittes gibt der Thread ein Signal, damit der nächste eventuell wartende Thread in den kritischen Abschnitt gelangen kann.
Betonen möchte ich, dass mit "schlafen" gemeint ist, dass der Thread vom Scheduler pausiert wird und so quasi keinerlei Rechenzeit beansprucht. Dies ist wesentlich effektiver als in einer den Prozessor stark belastenden Schleife, fortwährend zu überprüfen, ob man nun den kritischen Abschnitt betreten darf. Dies müsste mit einem anderen Verfahren durchgeführt werden.
Der Scheduler ist der Teil des Betriebssystems, welcher sich um die Zuteilung von Rechenzeit an die verschiedenen Threads kümmert.
Nun noch eine kurze Erläuterung zur Benennung dieses Konstruktes. Das Verfahren garantiert einen gegenseitigen Ausschluss. Nur ein Thread kann zur gleichen Zeit den kritischen Abschnitt betreten. Der englische Ausdruck für den gegenseitigen Ausschluss ist mutally exclusion. Aus dessen Buchstaben ist der Name MUT(ally)EX(clusion) entstanden.
Ein Mutex ist eine von mehreren Möglichkeiten, die Arbeit zweier Threads im Rahmen der "Synchronization Functions" zu synchronisieren.
Da auch ein Anwendung mindestens einen Thread hat, welche die vom Programmierer definierten Aufgaben durchführt, kann man die beschriebene Möglichkeit zur Thread-Synchronisation auch zur Prozesssynchronisation verwenden. Hierbei wird dann die Arbeit zweier Threads synchronisiert, welche nicht zum selben Prozess gehören.
Im folgenden Abschnitt werde ich mich nun etwas von der Theorie entfernen und die praktische Implementierung einer Klasse erläutern, welche die gewünschten API-Funktionen kapselt.
Praxis
Damit man in beiden Anwendungen Zugriff auf den selben Mutex hat, muss man ein gemeinsames Kennzeichen für den Mutex vereinbaren. Dies kann durch die Vergabe eines Namens erfolgen, durch welchen die API den Mutex identifizieren kann. Ein solcher Mutex heißt named-mutex.
Zur Erzeugung eines Mutex verwenden wir die API-Funktion CreateMutex. Diese erwartet drei Parameter.
Parameter | Beschreibung |
---|---|
lpMutexAttributes vom Typ SECURITY_ATTRIBUTES | Mehrere Einstellungen. Unter anderem ob das zurückgelieferte Handle vererbt werden darf. |
bInitialOwner vom Typ Boolean | Ob der aufrufende Thread den Mutex nach der Erstellung sofort besitzt. Dies kommt einem Aufruf von WaitForSingleObject direkt nach der Erstellung gleich. |
lpName vom Typ Long | Zeiger auf einen nullterminierten String, der den Names des Mutexobjektes angibt. Der Name darf keinen Slash "/" enthalten. Wenn kein Name angegeben wird, handelt es sich um einen anonymen Mutex, welcher nicht über einen Namen identifizierbar ist. |
Jede Handle die nicht mehr benötigt wird, sollte geschlossen werden. Hierfür muss die API-Funktion CloseHandle verwendet werden.
Da nur die Clientantwendung den Mutex erzeugen muss, brauchen wir für das Hauptprogramm nicht die CreateMutex-Funktion sondern die OpenMutex-Funktion, welche einem ein Handle auf einen schon eingerichteten Mutex zurückliefert.
Die OpenMutex-Funktion erwartet wie die CreateMutex-Funktion drei Parameter. Unterscheiden tut sich jedoch nur der erste Parameter. Dieser heißt hier dwDesiredAccess und sollte für die hier notwendigen Aktionen auf SYNCHRONIZE gesetzt werden. Hierdurch teilen wir Windows mit, dass wir den Mutex mit den Funktionen WaitForSingleObject und ReleaseMutex verwenden wollen.
Um einen Mutex, welchen man besitzt, wieder für andere Threads freizugeben, wird die ReleaseMutex-Funktion verwendet, welche den Handle des Mutex als Parameter erwartet.
Um einen Mutex in Besitz zu nehmen, wird die Funktion WaitForSingleObject verwendet. Diese erwartet als ersten Parameter den Handle auf den Mutex und als zweiten Parameter die Anzahl an Millisekunden, die gewartet werden soll, falls der Mutex nicht in Besitz genommen werden kann. Um zu warten bis man den Mutex besitzt kann die Konstante INFINITE verwendet werden.
Nun sind alle notwendigen Funktionen erklärt und es muss nur noch eine sinnvolle Verbindung aller Funktionen geschaffen werden.
Ablauf
Der Ablauf ist prinzipiell relativ einfach. Die Anwendung, auf welche gewartet werden soll, erstellt den Mutex und nimmt diesen so lange in Besitz bis sie die Aufgabe, auf welche das Hauptprogramm wartet, erledigt hat. Dann gibt sie den Mutex frei, sodass das Hauptprogramm weiterlaufen kann.
Das Hauptprogramm versucht beim Programmstart einen Handle für den Mutex zu bekommen. Sollte dies nicht klappen, da der Mutex noch nicht existiert, kann das Hauptprogramm veranlassen, dass das noch nicht laufende Programm gestartet wird. Nun sollte der Handle erfolgreich ermittelt werden können. Nun wird zunächst versucht den Mutex innerhalb von einer Millisekunde in Besitz zu nehmen. Sollte dies nicht gelingen, kann man nun dem Benutzer mitteilen, dass auf die Erledigung der Aufgaben des sekundären Programmes gewartet wird. Anschließend wird wieder versucht den Mutex in Besitz zu nehmen. Diesmal wird aber gewartet, bis man den Mutex besitzt, da man den Prozessor so wenig wie möglich belasten sollte. Alternativ kann man nur für jeweils 10 Sekunden probieren den Mutex in Besitz zu nehmen, um dem Benutzer zwischenzeitlich die Möglichkeit zu geben, die Aktion abzubrechen.
Sofern das Hauptprogramm nun den Mutex besitzt, kann es weiterarbeiten, da das sekundäre Programm seine Aufgaben erledigt hat.
Implementierung
In diesem Abschnitt stelle ich kurz die Funktionen, Eigenschaften und Ereignisse einer Mutexklasse vor, welche den praktischen Teil dieser Kolumne darstellt. Die Klasse sollte alle notwendigen Funktionen implementiert haben.
Eigenschaften
Die Klasse hat 2 Eigenschaften.
MutexHandle gibt den von der API zurückgegebenen Handle zurück. Dieser Handle ist nur für die aufrufende Anwendung gültig. Wenn also der MutexHandle zweier Anwendungen die selbe Ziffer trägt, so heißt dies noch lange nicht, dass es sich um ein Handle auf den selben Mutex handelt.
isLocked gibt einen Wahrheitswert(Boolean) zurück, der angibt, ob der Mutex von der aufrufenden Anwendung besitzt wird. Wenn der Mutex im Besitz der Anwendung ist, kann kein anderer Thread den Mutex in Besitz nehmen.
Methoden
Die Klasse exportiert 5 Methoden.
InitNamedMutex erstellt einen benannten Mutex.
OpenExistingMutex öffnet einen schon existierenden Mutex.
MutexLock versucht einen Mutex in Besitz zu nehmen; so lange es auch dauert.
MutexLockMS versucht einen Mutex in Besitz zu nehmen; es kann ein Timeout angegeben werden.
MutexUnlock gibt einen Mutex wieder frei, damit andere Threads den Mutex wieder in Besitz nehmen können.
Ereignisse
Die Klasse exportiert genau ein Ereignis zu Fehlerbehandlung.
Schlusswort und Download
Ich hoffe das beschriebene Verfahren ist verständlich, nachvollziehbar und vielleicht sogar für die Praxis nützlich.
In einer der nächsten Kolumnen wird es dann um das Thema Interprozesskommunikation gehen. Denn manchmal ist es sinnvoll Daten von Prozess zu Prozess zu übertragen, ohne z.B. eine Datei hierfür verwenden zu müssen.
MfG Florian Rittmeier
Ihre Meinung
Falls Sie Fragen zu diesem Artikel haben oder Ihre Erfahrung mit anderen Nutzern austauschen möchten, dann teilen Sie uns diese bitte in einem der unten vorhandenen Themen oder über einen neuen Beitrag mit. Hierzu können sie einfach einen Beitrag in einem zum Thema passenden Forum anlegen, welcher automatisch mit dieser Seite verknüpft wird.
Archivierte Nutzerkommentare
Klicken Sie diesen Text an, wenn Sie die 1 archivierten Kommentare ansehen möchten.
Diese stammen noch von der Zeit, als es noch keine direkte Forenunterstützung für Fragen und Kommentare zu einzelnen Artikeln gab.
Aus Gründen der Vollständigkeit können Sie sich die ausgeblendeten Kommentare zu diesem Artikel aber gerne weiterhin ansehen.
Kommentar von Stefan am 02.03.2005 um 21:55
Der COde ist fehlerhaft:
in den mutex functions müssen die strin ansi, nicht unicode übergeben werden. (strconv biegt das)
wollt nur darauf hinweisen,
grüsse,
stefan