Mehrfachstarts einer Windows-App verhindern — Benannte Mutexe und das Aktivieren des vorhandenen Fensters beim zweiten Start

· · Einzelinstanz-Erzwingung, Mutex, Windows, .NET, C#, SetForegroundWindow, Remotedesktop, Named Pipes, Windows-Entwicklung, Technische Beratung

„Ich habe die Business-App versehentlich ein zweites Mal gestartet, dieselbe Datei am Ende in zwei getrennten Fenstern bearbeitet und die Änderungen aus einem davon verloren.“ „Ein Hintergrund-Tool ist zweimal gestartet und hat am Ende dasselbe Ereignis doppelt verarbeitet.“ Mehrfachstarts einer Desktop-App klingen nach einem Randthema, sind aber in der Praxis eine überraschend häufige Quelle von Vorfällen. Die eigentliche Gegenmaßnahme ist einfach: einen benannten Mutex verwenden, um zu bestimmen, „bin ich die erste Instanz“.

Das Problem ist, dass diese einfache Implementierung von einer Reihe umgebender Fallstricke begleitet wird: eine Namensraumfalle, die den Schutz in Remotedesktop-Umgebungen außer Kraft setzt, für Mutex spezifische Freigaberegeln, die sich von anderen Synchronisierungsobjekten unterscheiden, sowie die Designfrage, wie man eine bereits laufende Instanz in den Vordergrund holt, was auf die Einschränkungen des Vordergrundfensters von Win32 trifft. Dieser Artikel arbeitet alles durch, von den Grundlagen der Mehrfachstart-Erkennung mit einem benannten Mutex bis zu den umgebenden Designentscheidungen, die in der Produktion tatsächlich zählen.

1. Die Kurzfassung

  • Die Grundform der Mehrfachstart-Erkennung ist new Mutex(true, name, out bool createdNew). Es wird keine Ausnahme geworfen, selbst wenn bereits ein Mutex mit demselben Namen existiert – createdNew kommt einfach als false zurück, sodass Sie anhand dieses Werts verzweigen.1
  • Geben Sie dem Namen kein Präfix, wird er standardmäßig im (sitzungsgebundenen) Namensraum Local\ erzeugt. In einer Remotedesktop-Umgebung, in der derselbe Benutzer mehrere Sitzungen haben kann, wird jede Sitzung so behandelt, als hätte sie ihren eigenen separaten Mutex, und die Mehrfachstart-Verhinderung funktioniert nicht. Möchten Sie eine einzige Instanz über Sitzungen hinweg, ist das Präfix Global\ zwingend erforderlich.23
  • Ein Mutex kann nur von demselben Thread freigegeben werden, der ihn erworben hat. Der Aufruf von ReleaseMutex von einem anderen Thread aus wirft ApplicationException. Wird der besitzende Prozess beendet, ohne ihn freizugeben, erhält der nächste Thread, der den Mutex erwirbt, eine AbandonedMutexException – das ist jedoch tatsächlich ein Signal, dass das Warten selbst erfolgreich war, und die korrekte Behandlung besteht darin, die Konsistenz des Zustands vor der Verwendung zu prüfen, statt sie stillschweigend zu schlucken.45
  • Sobald Sie wissen, „eine Instanz läuft bereits“, besteht die eigentliche Arbeit darin, das Fenster der vorhandenen Instanz nach vorne zu holen. SetForegroundWindow wird vom Betriebssystem für andere Aufrufer als den Vordergrundprozess eingeschränkt, sodass ein naiver Aufruf fehlschlägt und Sie am Ende nur eine blinkende Taskleistenschaltfläche haben.6 Die Standardtechnik besteht darin, den neu gestarteten zweiten Prozess seine eigene „Berechtigung, den Vordergrund zu setzen“ über AllowSetForegroundWindow an die vorhandene Instanz übergeben zu lassen.7
  • Um Startargumente (etwa einen zu öffnenden Dateipfad) an die vorhandene Instanz weiterzugeben, ist das Weiterleiten über eine benannte Pipe der Standardansatz. Den Vergleich der IPC-Mechanismen überlassen wir „Wie man die Interprozesskommunikation unter Windows wählt“ und konzentrieren diesen Artikel auf das Design „Erkennen → Benachrichtigen → Aktivieren“.
  • Konsolen-Apps und Dienste passen nicht direkt zum Design dieses Artikels. Dienste funktionieren von Anfang an anders, da der SCM ohnehin immer nur eine Instanz eines gegebenen Dienstnamens startet (Kapitel 8).

2. Die Grundlagen der Erkennung mit einem benannten Mutex

Ein Mutex ist ein Kernelobjekt; erzeugen Sie ihn mit einem Namen, können andere Prozesse, die diesen Namen kennen, auf dasselbe Objekt verweisen. Die Mehrfachstart-Erkennung stützt sich ausschließlich auf dieses „Teilen des Namens“.

using var mutex = new Mutex(initiallyOwned: true, name: MutexName, out bool createdNew);
if (!createdNew)
{
    // Läuft bereits
    return;
}
// Dieser Thread besitzt den Mutex nur, wenn createdNew true ist

Zwei Punkte sind hier zu beachten.

  • Es wird keine Ausnahme geworfen, selbst wenn bereits ein Mutex mit demselben Namen existiert. createdNew kommt einfach als false zurück, und Sie erhalten eine Referenz auf das vorhandene Objekt zurück. Verzweigen Sie immer anhand von createdNew.1
  • initiallyOwned: true wirkt sich nur aus, wenn tatsächlich ein neues Objekt erzeugt werden konnte. Existierte bereits eines (createdNew == false), wird dieser Thread nicht automatisch zum Besitzer. Dieser Nebeneffekt spielt für die Mehrfachstart-Erkennung keine Rolle, aber um den Unfall zu verhindern, dass „der zweite Prozess versehentlich ReleaseMutex aufruft“, ist es am sichersten, wenn der Zweig, bei dem createdNew false ist, den Mutex überhaupt nicht anfasst und einfach sofort beendet.1

Die übliche Praxis für den Namen ist, eine produktspezifische GUID einzubetten, damit es nicht mit Apps anderer Anbieter kollidiert (z. B. "KomuraSoft.MyApp.SingleInstance.{3F1E2B10-...}"). Genau wie beim Besetzen von Pipe-Namen ist der Namensraum für benannte Kernelobjekte für andere Prozesse auf dem Rechner sichtbar, sodass es Sinn ergibt, den Namen schwer erratbar zu machen.

3. Global\ vs. Local\ — Was über Sitzungen hinweg geschieht

Die Namensräume von Windows-Kernelobjekten teilen sich auf in sitzungsgebundene Namensräume und einen einzigen globalen Namensraum, der systemweit geteilt wird. Erzeugen Sie einen benannten Mutex, ohne ein Präfix anzugeben, wird er standardmäßig im Sitzungsnamensraum des Aufrufers (Local\) erzeugt; nur wenn Sie Global\ hinzufügen, wird er im über alle Sitzungen geteilten Namensraum erzeugt.3 Die .NET-Klasse Mutex folgt demselben Verhalten, und ihre Dokumentation stellt ausdrücklich fest, dass „ein benannter Mutex ohne Präfix standardmäßig Local\ verwendet“.2

Das wird in Szenarien wie diesen zum Problem:

  • Ein Benutzer meldet sich direkt über die Konsole an einem geschäftlichen Verwaltungsrechner an, verbindet sich aber gleichzeitig für sich selbst über Remotedesktop als weitere Sitzung mit demselben Rechner (im Betrieb üblich).
  • Derselbe Benutzer trennt und verbindet eine RDP-Sitzung wiederholt neu, und jede erneute Verbindung erhält eine neue Sitzungs-ID.

Belassen Sie den Mutex bei Local\ (kein Präfix), werden diese Fälle als getrennte Sitzungen behandelt, und die Mehrfachstart-Verhinderung arbeitet in jeder Sitzung unabhängig. Anders gesagt erhalten Sie den Fehler, dass „derselbe Benutzer aus einer zweiten Sitzung heraus problemlos eine zweite Kopie starten kann“ – die Mehrfachstart-Verhinderung funktioniert nicht wie beabsichtigt. Umgekehrt entsteht bei gewöhnlicher Desktop-Nutzung, die nur eine einzige Sitzung voraussetzt, durch das Verbleiben bei Local\ (dem Standard) kein echter Schaden.

Das allgemeine Design von Rechnern, die von mehreren Benutzern geteilt werden, wird auch in „Einführung in Windows-Benutzerprofile“ behandelt, doch mit Fokus speziell auf das Thema dieses Artikels ist Vorsicht geboten. Global\ ist ein einziger Namensraum, der von jedem Benutzer und jeder Sitzung auf dem Rechner geteilt wird – das bedeutet nicht automatisch „eine Instanz pro Benutzer“. Fügen Sie bei festem Namen einfach Global\ hinzu, etwa Global\KomuraSoft.MyApp.SingleInstance, wird während Benutzer A die App ausführt auch der Startversuch von Benutzer B durch denselben Mutex blockiert – was faktisch „eine Instanz für den gesamten Rechner“ ergibt (Zeile 3 der Tabelle in Kapitel 5). Möchten Sie „eine Instanz pro Benutzer, aber über dessen mehrere Sitzungen hinweg konsolidiert“, müssen Sie Global\ mit einem benutzerspezifischen Bezeichner (etwa einer SID) kombinieren, der in den Namen eingebettet wird. Möchten Sie umgekehrt tatsächlich auf eine einzige Instanz für den gesamten Rechner beschränken (geteiltes Verhalten für alle Benutzer ist beabsichtigt), ist es in Ordnung, bei Global\ ohne SID zu bleiben.

4. Mutex-Freigaberegeln — Besitzende Threads und AbandonedMutexException

Mutex hat eine Einschränkung, die andere Synchronisierungsobjekte wie Semaphore und AutoResetEvent nicht haben: Er erzwingt die Thread-ID, die ihn erworben hat, was bedeutet, dass er nur von demselben Thread freigegeben werden kann, der ihn erworben hat.2 Der Aufruf von ReleaseMutex von einem anderen Thread aus wirft ApplicationException („der aufrufende Thread besitzt den Mutex nicht“).4

In Code, der async/await verwendet, kann die Ausführung nach einem await auf einem anderen Thread-Pool-Thread fortgesetzt werden. Seien Sie vorsichtig: Code zu schreiben, der einen Mutex erwirbt und ihn dann, direkt nach dem Einschieben asynchroner Arbeit, in der Fortsetzung freigibt, kann diese Einschränkung still verletzen. Für die Mehrfachstart-Erkennung ist es am sichersten, sowohl Erwerb als auch Freigabe innerhalb kurzen, synchronen Codes zu halten.

Ein weiterer zu beachtender Punkt ist die Verwaisung (Abandonment). Wird der Thread, dem ein Mutex gehörte, beendet, ohne ReleaseMutex aufzurufen (der Prozess ist abgestürzt, durch eine unbehandelte Ausnahme gestorben usw.), gerät dieser Mutex in einen verwaisten Zustand. Der nächste Thread, der diesen Mutex erwirbt, erhält eine AbandonedMutexException, aber diese Ausnahme zeigt tatsächlich an, dass das Warten selbst erfolgreich war und der Aufrufer bereits Besitzer des Mutex ist.5 Das kommt für reine Mehrfachstart-Erkennung typischerweise nicht vor (da Sie ihn erwerben und bis zum Prozessende halten, ohne ihn freizugeben), aber verwenden Sie denselben Mutex auch für andere gegenseitige Ausschlusszwecke wieder, sollten Sie wie folgt vorgehen, im Bewusstsein, dass der geschützte Zustand inkonsistent hinterlassen worden sein könnte:

try
{
    if (mutex.WaitOne(TimeSpan.FromSeconds(5)))
    {
        // Normale Verarbeitung
    }
}
catch (AbandonedMutexException)
{
    // Das Warten war erfolgreich, und dieser Thread besitzt den Mutex bereits.
    // Den geschützten Zustand vor der Verwendung prüfen oder sicher neu initialisieren
}

Die Entscheidung, „die Ausnahme ignorieren, den Zustand prüfen und fortfahren, oder aufgeben und abnormal beenden“, deckt sich direkt mit dem Gedankengang in der „Entscheidungstabelle: Beenden oder Fortfahren bei unerwarteten Ausnahmen“ dieses Blogs. AbandonedMutexException ist eine Ausnahme, die Ihnen ausdrücklich „den Bereich, der beschädigt sein könnte“ mitteilt, sodass die grundlegende Richtlinie ist, genau diesen Bereich (die geschützten Daten) zu prüfen, statt die Ausnahme zu schlucken.

Außerdem sollten Sie auf dem Pfad des normalen Beendens ReleaseMutex explizit in einem finally-Block aufrufen. Verschwindet der besitzende Thread beim Beenden des Prozesses, wird der Mutex als „verwaist“ behandelt, sodass Sie ohne explizite Freigabe beim nächsten Start der App eine unnötige AbandonedMutexException verursachen.

5. Entscheidungstabelle — Den Geltungsbereich für Ihren Mutex wählen

Der Namensraum und das Design auf Pipe-Seite ändern sich je nach der „Einheit“, auf der Sie Mehrfachstarts verhindern möchten.

Einheit Angenommenes Szenario Mutex-Namensraum Weitergabe von Startargumenten Vorbehalte
Pro Sitzung (Standard) Gewöhnliche Desktop-Nutzung ohne RDP, oder wo „eine pro Sitzung“ genügt Kein Präfix (d. h. Local\) CurrentUserOnly plus die Sitzungs-ID im Pipe-Namen einbeziehen Kann Starts aus einer anderen Sitzung in einer RDP-kombinierten Umgebung nicht verhindern. Hat derselbe Benutzer mehrere Sitzungen, verursacht ein fester Pipe-Name ohne Sitzungs-ID eine Kollision (benannte Pipes unterliegen nicht dem Sitzungsnamensraum des Mutex)
Pro Benutzer (sitzungsübergreifend) Derselbe Benutzer wechselt zwischen Konsole und RDP, oder verbindet sich über RDP wiederholt neu Global\ + die SID des Benutzers in den Namen einbetten + explizite ACL über MutexSecurity setzen CurrentUserOnly (anhand der Benutzer-SID beurteilt, funktioniert daher über Sitzungen hinweg) Der in der Praxis am häufigsten benötigte Fall. Nur Global\ ohne SID zu verwenden erzeugt unbeabsichtigt ein maschinenweites Verhalten (nächste Zeile). Da die SID keine geheime Information für den Benutzer ist, sollten Sie auf gemeinsam genutzten PCs / RDS-Umgebungen auch Namensbesetzung durch andere Benutzer berücksichtigen und mit einer ACL schützen
Pro Rechner (über alle Benutzer hinweg) Lizenzierung erlaubt nur eine Instanz pro Rechner, oder eine gemeinsame Ressource muss über alle Benutzer hinweg exklusiv sein Global\ (ohne benutzerspezifische Information) + explizite ACL über MutexSecurity setzen CurrentUserOnly weglassen und erlaubte Benutzer explizit über PipeSecurity angeben In Remotedesktopdienste-Umgebungen, in denen mehrere Benutzer gleichzeitig angemeldet sind, führt das tendenziell zu für das Geschäft unerwünschtem Verhalten – prüfen Sie daher, ob es den Anforderungen entspricht. Leiten Sie Startargumente eines anderen Benutzers nicht unverändert direkt an das Fenster des ersten Benutzers weiter. Das kann Dateipfade preisgeben oder das Dokument einer anderen Person in der Sitzung eines unbeabsichtigten Benutzers öffnen – weisen Sie Anfragen anderer Benutzer zurück, oder entwerfen Sie es rund um einen Broker ohne UI neu

Als Ergänzung: Benannte Pipes unterliegen nicht der Local\/Global\-Sitzungsnamensräumlichkeit wie Mutex, und sind standardmäßig sitzungsübergreifend erreichbar. Anders gesagt erreicht selbst ohne CurrentUserOnly allein durch übereinstimmenden Pipe-Namen die Verbindung selbst eine vorhandene Instanz in einer anderen Sitzung. Hier geht es bei CurrentUserOnly nicht um Erreichbarkeit, sondern um Autorisierung – es ist eine Zugriffskontrolle, die eingrenzt, „wessen Verbindungen erlaubt werden“, auf den aktuellen Benutzer (und dieselbe Erhöhungsstufe). Setzen Sie es nicht, gewährt der standardmäßige Sicherheitsdeskriptor auch Everyone Lesezugriff, sodass auch Verbindungen von unbeabsichtigten anderen Benutzern durchkämen. Bei einem Design, das „pro Benutzer, sitzungsübergreifend“ mit einem Global\ + SID Mutex erreicht, ist es angemessen, auch der Benachrichtigungspipe CurrentUserOnly hinzuzufügen und den verbindenden Teilnehmer auf dasselbe „nur den Zielbenutzer“ wie beim Mutex einzugrenzen. Glücklicherweise wird PipeOptions.CurrentUserOnly anhand der SID des Benutzers (und der Erhöhungsstufe) statt der Sitzungs-ID beurteilt, sodass es sich sauber mit Zeile 2 der obigen Tabelle (pro Benutzer, sitzungsübergreifend) verbindet.

Noch etwas: Verwenden Sie für den Fall pro Rechner (Zeile 3 der Tabelle) einen Global\ Mutex mit festem Namen, müssen Sie auch auf Namensbesetzung (Squatting) achten. Versuchen Sie, eine App mithilfe eines benannten Mutex auf eine einzige Instanz zu beschränken, könnte ein böswilliger Benutzer vorab einen Mutex mit demselben Namen erzeugen und den Start der App stören.8 Eine explizite ACL zum Erzeugungszeitpunkt über MutexSecurity (MutexAcl.Create) zu setzen, schützt davor, dass ein anderer Benutzer den Mutex kapert oder unrechtmäßig hält, sobald Sie es geschafft haben, ihn zuerst zu erzeugen. Beachten Sie jedoch, dass das eine Verteidigung gegen „nachträgliche Störung“ ist – es verhindert in keiner Weise, „zuerst besetzt zu werden“. Die ACL wirkt sich erst aus, sobald Sie den Mutex neu erzeugen; hat ein böswilliger Benutzer bereits vor Ihnen einen Mutex unter demselben Namen erzeugt, öffnet Ihr Aufruf (wie ausgefeilt auch immer die vorbereitete ACL ist) einfach das vorhandene, von ihm eingerichtete Objekt, und Sie sind an dessen konfigurierte ACL gebunden. Einen von vornherein schwer erratbaren Namen zu haben, hat für sich genommen durchaus Wert, aber möchten Sie einen böswilligen Benutzer mit lokalen Code-Ausführungsrechten vollständig aussperren, verlassen Sie sich nicht allein auf die Namensbelegung des Mutex – ziehen Sie in Betracht, sie mit einem weiteren Ausschlussmechanismus zu kombinieren, etwa einer Sperrdatei unter einem pro Benutzer geschützten Verzeichnis.9

Noch ein Punkt: CurrentUserOnly hat eine leicht zu übersehende Einschränkung. Unter Windows erlaubt es eine Verbindung nur, wenn nicht nur das Benutzerkonto übereinstimmt, sondern auch die Erhöhungsstufe (ob Sie als Administrator ausgeführt werden).10 Es ist verlockend anzunehmen, dass „da der Name des Mutex allein aus der SID des Benutzers aufgebaut ist, die Erkennung selbst unabhängig von der Erhöhung funktioniert“ – aber verwenden Sie einen Mutex mit expliziter ACL, wirkt sich auch das auf die Erhöhung aus. Windows versieht standardmäßig Objekte, die von einem Prozess erzeugt wurden, der auf hoher Integritätsstufe läuft (als Administrator ausgeführt), mit einem Label hoher Integritätsstufe und verweigert schreibenden Zugriff von Prozessen auf niedrigerer Integritätsstufe. Der Mutex/MutexAcl.Create von .NET fordert intern zusätzlich zu SYNCHRONIZE und MUTEX_MODIFY_STATE auch DELETE/READ_CONTROL/WRITE_DAC/WRITE_OWNER (STANDARD_RIGHTS_REQUIRED) an, sodass der Aufruf zur Erzeugung/zum Öffnen des Mutex selbst eine UnauthorizedAccessException werfen kann, wenn Sie die erste Instanz „als Administrator“ starten und dann eine zweite Instanz mit normalen Rechten starten. Anders gesagt kann die Annahme aus Kapitel 7, dass „nur die Benachrichtigungspipe von der ACL abgewiesen wird, während die Mehrfachstart-Verhinderung selbst gelingt“, zusammenbrechen, und eine Ausnahme kann bereits früher geworfen werden, bevor die Erkennung überhaupt stattfindet. Behandeln Sie auch diesen Pfad als Signal, dass „bereits eine andere Instanz läuft oder sich die Erhöhungsstufe unterscheidet, sodass es mit den normalen Mitteln nicht bestimmt werden kann“ – es ist angemessen, den Aufruf von Mutex/MutexAcl.Create selbst in try/catch (UnauthorizedAccessException) einzuschließen und bei einer Ausnahme den Start aufzugeben und still zu beenden (oder, ähnlich wie „es ist in Ordnung, wenn die Benachrichtigung nicht ankommt“ in Kapitel 7, für die Mehrfachstart-Verhinderung auf der sicheren Seite zu bleiben). Benötigen Sie Benachrichtigung auch über Erhöhungsstufen hinweg, wechseln Sie von CurrentUserOnly zu einem Design, das eine SID-basierte ACL über PipeSecurity explizit aufbaut.

6. Die vorhandene Instanz nach vorne holen — Die Einschränkungen von SetForegroundWindow

Sobald Sie über Mutex festgestellt haben, dass „bereits eine Instanz läuft“, möchten die meisten Apps das Fenster der vorhandenen Instanz nach vorne holen. Ein einfacher Aufruf von SetForegroundWindow gegen den vorhandenen Prozess schlägt in den meisten Fällen fehl.

Windows schränkt streng ein, welche Prozesse das Vordergrundfenster setzen dürfen. Laut offizieller Dokumentation holt SetForegroundWindow das Fenster nicht tatsächlich nach vorne, sofern der aufrufende Prozess nicht eine der folgenden Bedingungen erfüllt – es lässt lediglich die Taskleistenschaltfläche blinken.6

  • Der aufrufende Prozess selbst ist der aktuelle Vordergrundprozess
  • Der aufrufende Prozess wurde vom Vordergrundprozess gestartet
  • Der aufrufende Prozess hat das jüngste Eingabeereignis erhalten
  • Es gibt derzeit kein Vordergrundfenster
  • Der Vordergrundprozess oder der aufrufende Prozess wird debuggt

Im Szenario der Mehrfachstart-Erkennung erfüllt die vorhandene Instanz (der erste Prozess, im Hintergrund laufend) typischerweise keine dieser Bedingungen. Andererseits hat der zweite Prozess, den der Benutzer gerade per Doppelklick gestartet hat, in vielen Fällen gerade ein Eingabeereignis erhalten und besitzt die Berechtigung, den Vordergrund zu setzen. Diese Asymmetrie auszunutzen, ist die gängige Praxis in der Produktion.

AllowSetForegroundWindow ist eine API, die es einem Prozess, der den Vordergrund setzen kann, erlaubt, diese Berechtigung an einen anderen Prozess zu übergeben.7 Gibt der zweite Prozess seine eigene Berechtigung mit ASFW_ANY auf (gewährt sie allen Prozessen) und fordert dann über eine benannte Pipe die Aktivierung von der vorhandenen Instanz an, gelingt der SetForegroundWindow-Aufruf der vorhandenen Instanz.

[DllImport("user32.dll")]
private static extern bool AllowSetForegroundWindow(int dwProcessId);

private const int ASFW_ANY = -1;

// Unter der Annahme, dass der zweite Prozess (dieser hier) die
// Vordergrund-Setzberechtigung besitzt, diese bedingungslos übergeben.
// Das lässt den SetForegroundWindow-Aufruf auf Seite der vorhandenen
// Instanz gelingen
AllowSetForegroundWindow(ASFW_ANY);

Es bleiben Fälle, die selbst das nicht retten kann (etwa ein Start über die Aufgabenplanung, bei dem auch der zweite Prozess selbst keine Vordergrundberechtigung besitzt). In diesen Fällen ist es ein vernünftiges Design, bei der Benachrichtigung über die blinkende Taskleiste zu bleiben und das Fenster nicht zwangsweise nach vorne zu holen. Als Benutzerbenachrichtigung ist es in Ordnung, immer eine Toast-Benachrichtigung zu verwenden und WindowState von Minimized auf Normal zurückzusetzen, während der abschließende Schritt „nach vorne holen“ als „wenn möglich tun“ statt als etwas behandelt wird, das zwingend gelingen muss.

7. Startargumente an die vorhandene Instanz weitergeben — Benannte Pipes

Über die reine Mehrfachstart-Erkennung hinaus ist es auch eine übliche Anforderung, dass „wird die App mit einer zu öffnenden Datei als Argument gestartet, sollte die vorhandene Instanz diese Datei öffnen“. WM_COPYDATA (der klassische Mechanismus zum Senden von Daten über eine Fensternachricht) existiert auch für diesen Zweck als Option, aber der Aufwand, ein Fensterhandle zu erhalten und die Nachricht zu marshallen, überwiegt den Nutzen, sodass es für neue Designs unkomplizierter ist, eine benannte Pipe zu verwenden.

Das Design ist einfach: Der zweite Prozess, der über den Mutex festgestellt hat, dass „bereits eine Instanz läuft“, verbindet sich mit der benannten Pipe, auf der die vorhandene Instanz lauscht, und sendet die Kommandozeilenargumente, etwa als JSON serialisiert. Implementierungsvorbehalte, die spezifisch für benannte Pipes selbst sind – das Entgegenwirken von Pipe-Namen-Besetzung, Zugriffskontrolle über CurrentUserOnly und Ähnliches – werden im Abschnitt zu benannten Pipes von „Wie man die Interprozesskommunikation unter Windows wählt“ behandelt – folgen Sie dem dort. Der einzige Vorbehalt, der spezifisch für den Kontext der Mehrfachstart-Erkennung ist, ist der folgende:

  • Schlägt das Senden der Benachrichtigung fehl, ist es dennoch in Ordnung, das als Erfolg für die Mehrfachstart-Verhinderung zu behandeln. Es gibt Timing-Probleme, bei denen die Benachrichtigung nicht durchkommt, etwa weil sich die vorhandene Instanz gerade mitten im Herunterfahren befindet und ihren Pipe-Server geschlossen hat. Selbst dann wurde das primäre Ziel – „lass den zweiten Prozess nicht starten“ – dennoch erreicht, sodass es nicht nötig ist, allein wegen der fehlgeschlagenen Benachrichtigung einen Fehlerdialog anzuzeigen.

8. Wie sich das bei Konsolen-Apps und Diensten unterscheidet

Das Design in diesem Artikel setzt eine Desktop-App mit Fenster voraus. Für Konsolen-Apps und Batch-Tools ist es oft realistischer, für „sicheres Koexistieren auch bei mehrfacher gleichzeitiger Ausführung“ zu entwerfen statt für „Mehrfachstarts verhindern“, was sich faktisch auf die Frage der dateibasierten Ausschlusssteuerung reduziert.

Windows-Dienste sind eine ganz andere Angelegenheit. Da der Dienststeuerungs-Manager (SCM) von vornherein nie zwei Instanzen eines Dienstes mit demselben Namen startet, ist die in diesem Artikel behandelte Mutex-basierte Erkennung grundsätzlich unnötig. Bei Konfigurationen wie „eine UI-App plus ein residenter Dienst“ würden Sie das Design dieses Artikels nur für die UI-Seite verwenden; wie man über Mehrfachstarts/-ausführungen auf der Dienstseite denkt, ist ein separates Thema. Wie man einen Dienst baut und dessen Designüberlegungen überlassen wir einem anderen Artikel.

9. Ein Implementierungsbeispiel — Erkennung und Aktivierungsanfrage über Mutex

Das ist ein praxistauglicher Minimalaufbau, der die Inhalte der Kapitel 2 bis 7 zusammenführt. Er setzt WPF voraus, funktioniert aber fast unverändert auch für WinForms, indem man lediglich Application.Current.Dispatcher durch Control.Invoke ersetzt.

using System.IO.Pipes;
using System.Runtime.InteropServices;
using System.Security.AccessControl;
using System.Security.Principal;
using System.Text.Json;

public static class Program
{
    // Eine produktspezifische GUID einbetten, damit der Name nicht mit anderen
    // Apps kollidiert. Da wir auf eine einzige Instanz "pro Benutzer,
    // sitzungsübergreifend" beschränken möchten, betten wir zusätzlich zu
    // Global\ die SID des Benutzers in den Namen ein. Verwenden Sie Global\
    // allein ohne die SID, teilen sich alle Benutzer denselben Mutex, was zu
    // "eine Instanz für den gesamten Rechner" wird (Kapitel 3)
    private static readonly string MutexName =
        $@"Global\KomuraSoft.MyApp.SingleInstance.{{3F1E2B10-9C2E-4B7E-8B1E-8B4F2D6A5C10}}.{WindowsIdentity.GetCurrent().User}";
    // Den Geltungsbereich der Benachrichtigungspipe mit dem des Mutex (pro
    // Benutzer) angleichen. Behalten wir einen festen Namen ohne die SID,
    // kann das kollidieren/übersprechen, wenn ein anderer Benutzer unter
    // demselben Namen einen Server aufsetzt
    // (Kapitel 5; CurrentUserOnly grenzt nur die ACL ein, es trennt keine Namen)
    private static readonly string PipeName =
        $"KomuraSoft.MyApp.Activate.{{3F1E2B10-9C2E-4B7E-8B1E-8B4F2D6A5C10}}.{WindowsIdentity.GetCurrent().User}";

    [STAThread]
    private static void Main(string[] args)
    {
        // Eine explizite ACL zu setzen, die nur dem aktuellen Benutzer volle
        // Kontrolle gewährt, verhindert Kapern/Störung durch andere Benutzer,
        // sobald wir es geschafft haben, ihn zuerst zu erzeugen (erfordert
        // das NuGet-Paket System.Threading.AccessControl). Das schützt jedoch
        // nicht davor, zuerst besetzt zu werden (Kapitel 5)
        var mutexSecurity = new MutexSecurity();
        mutexSecurity.AddAccessRule(new MutexAccessRule(
            WindowsIdentity.GetCurrent().User!, MutexRights.FullControl, AccessControlType.Allow));

        bool createdNew;
        Mutex mutex;
        try
        {
            // Dieser Aufruf erwirbt den Mutex nur tatsächlich, wenn createdNew true ist
            mutex = MutexAcl.Create(
                initiallyOwned: true, name: MutexName, createdNew: out createdNew, mutexSecurity: mutexSecurity);
        }
        catch (UnauthorizedAccessException)
        {
            // Selbst bei demselben Benutzer kann eine abweichende
            // Erhöhungsstufe dazu führen, dass das Öffnen eines vorhandenen
            // Mutex selbst verweigert wird (Kapitel 5). Das als "bereits
            // eine andere Instanz auf anderer Erhöhungsstufe vorhanden"
            // behandeln, die Benachrichtigung aufgeben und auf der sicheren
            // Seite bleiben (nicht starten)
            return;
        }
        using var _ = mutex;

        if (!createdNew)
        {
            // Läuft bereits. Die vorhandene Instanz benachrichtigen und sich selbst beenden
            NotifyRunningInstanceAsync(args).GetAwaiter().GetResult();
            return;
        }

        try
        {
            // Sicherstellen, dass das StartupUri von App.xaml entfernt wurde.
            // Bleibt es bestehen, wird zusätzlich zu dem hier manuell
            // erstellten MainWindow auch das StartupUri-basierte Fenster
            // automatisch erzeugt und angezeigt (wobei auch app.MainWindow
            // überschrieben wird), sodass zwei Fenster geöffnet bleiben und
            // die Aktivierungsanfrage möglicherweise auf das falsche zeigt
            var app = new App();
            app.InitializeComponent();
            var mainWindow = new MainWindow();
            app.MainWindow = mainWindow;
            mainWindow.Show();

            // Den Pipe-Server erst starten, nachdem MainWindow erstellt und
            // angezeigt wurde. In umgekehrter Reihenfolge riskiert man, dass
            // eine Aktivierungsanfrage eintrifft, bevor das Fenster
            // existiert, was den Aufruf von ActivateMainWindow scheitern
            // lassen kann (die Ausnahme wird vom Catch-All unten geschluckt,
            // sodass die Benachrichtigung einfach verschwindet). Eine
            // Anfrage, die während dieses Fensters eintrifft, fällt auf das
            // Best-Effort-Design in NotifyRunningInstanceAsync zurück
            // ("Server noch nicht gestartet -> Verbindungsfehler", Kapitel 7)
            StartActivationServer();

            app.Run();
        }
        finally
        {
            // Vor dem Beenden explizit vom besitzenden Thread (diesem
            // Thread) freigeben. Wird vor dem Beenden nicht freigegeben,
            // verursacht das beim nächsten Start eine unnötige
            // AbandonedMutexException (Kapitel 4)
            mutex.ReleaseMutex();
        }
    }

    private const int ASFW_ANY = -1;

    [DllImport("user32.dll")]
    private static extern bool AllowSetForegroundWindow(int dwProcessId);

    private static async Task NotifyRunningInstanceAsync(string[] args)
    {
        try
        {
            using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(3));
            using var pipe = new NamedPipeClientStream(
                ".", PipeName, PipeDirection.Out, PipeOptions.Asynchronous | PipeOptions.CurrentUserOnly);
            await pipe.ConnectAsync(cts.Token);

            // Wir (der zweite Prozess, gerade gestartet) haben wahrscheinlich
            // gerade ein Eingabeereignis erhalten und besitzen oft die
            // Vordergrund-Setzberechtigung. Diese Berechtigung bedingungslos
            // übergeben, damit der SetForegroundWindow-Aufruf der
            // vorhandenen Instanz gelingt (Kapitel 6)
            AllowSetForegroundWindow(ASFW_ANY);

            byte[] payload = JsonSerializer.SerializeToUtf8Bytes(new ActivateRequest(1, args));
            await pipe.WriteAsync(payload, cts.Token);
        }
        catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or OperationCanceledException)
        {
            // Ob die vorhandene Instanz wegen laufenden Herunterfahrens nicht
            // antwortet (IOException) oder die CurrentUserOnly-Autorisierung
            // wegen abweichender Erhöhungsstufe fehlschlägt
            // (UnauthorizedAccessException; siehe Hinweis in Kapitel 5) -
            // wir unterscheiden den Grund für die ausbleibende Benachrichtigung
            // nicht - das primäre Ziel der Mehrfachstart-Verhinderung
            // (den zweiten nicht starten lassen) wurde dennoch erreicht (Kapitel 7)
        }
    }

    private static void StartActivationServer()
    {
        // Obergrenze für eine einzelne Nachricht. Schützt den Speicher des
        // Servers, selbst wenn ein veralteter Helfer desselben Benutzers
        // oder ein defekter Client unbegrenzt weitersendet
        const int MaxPayloadBytes = 64 * 1024;

        _ = Task.Run(async () =>
        {
            while (true)
            {
                try
                {
                    // Auch den Konstruktor selbst innerhalb des try
                    // platzieren. Da maxNumberOfServerInstances 1 ist, kann
                    // dies in einem Moment, in dem das Aufräumen der
                    // vorigen Verbindung noch nicht abgeschlossen ist, eine
                    // IOException werfen; würde man es außerhalb des try
                    // platzieren, würde dieser eine Fehlschlag den gesamten
                    // Hintergrund-Task mit sich reißen
                    using var pipe = new NamedPipeServerStream(
                        PipeName, PipeDirection.In, 1,
                        PipeTransmissionMode.Byte, PipeOptions.Asynchronous | PipeOptions.CurrentUserOnly);

                    await pipe.WaitForConnectionAsync();

                    // Da maxNumberOfServerInstances 1 ist, könnten wir,
                    // bliebe diese Verbindung an einem nicht antwortenden
                    // Client hängen, überhaupt keine weiteren legitimen
                    // Startanfragen mehr entgegennehmen. Eine Zeitgrenze für
                    // eine einzelne Verbindung setzen
                    using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
                    using var ms = new MemoryStream();
                    var buffer = new byte[4096];
                    int n;
                    while ((n = await pipe.ReadAsync(buffer, cts.Token)) > 0)
                    {
                        ms.Write(buffer, 0, n);
                        if (ms.Length > MaxPayloadBytes)
                            throw new IOException("Die Nutzlast hat die maximale Größe überschritten.");
                    }

                    var req = JsonSerializer.Deserialize<ActivateRequest>(ms.ToArray());
                    // Sowohl Args als auch Version validieren. Ein Absender
                    // einer alten Version oder eine handgestrickte
                    // fehlerhafte Nutzlast könnte JSON wie {"Version":1}
                    // senden, dem Args fehlt, was mit auf null belassenem
                    // Args deserialisiert wird
                    if (req is { Version: 1, Args: not null })
                    {
                        // UI-Operationen zurück auf dem UI-Thread ausführen
                        Application.Current.Dispatcher.Invoke(() => ActivateMainWindow(req.Args));
                    }
                }
                catch (Exception)
                {
                    // Das als Fehlschlag dieser einen Verbindung schlucken,
                    // unabhängig vom Grund - eine abgebrochene Verbindung,
                    // Überschreiten der Zeit- oder Größengrenze, eine
                    // beschädigte Nutzlast, eine unerwartete Ausnahme
                    // während des Dispatch usw. Würde man diesen Task
                    // selbst sterben lassen, könnten keine weiteren
                    // Benachrichtigungen mehr entgegengenommen werden,
                    // daher muss die Schleife immer fortgesetzt werden.
                    // Um jedoch ein Hot-Spinning zu vermeiden, falls das
                    // Konstruieren der Pipe selbst unmittelbar vor dem
                    // await wiederholt sofort fehlschlägt (z. B. ein
                    // anderer Prozess hält den Einzelinstanz-Platz), immer
                    // vor der nächsten Schleifeniteration kurz innehalten
                    await Task.Delay(TimeSpan.FromSeconds(1));
                }
            }
        });
    }

    [DllImport("user32.dll")]
    private static extern bool SetForegroundWindow(IntPtr hWnd);

    private static void ActivateMainWindow(string[] args)
    {
        var window = Application.Current.MainWindow;
        if (window is null) return;

        if (window.WindowState == System.Windows.WindowState.Minimized)
            window.WindowState = System.Windows.WindowState.Normal;
        window.Show();
        window.Activate();

        // WPFs Activate() ruft intern SetForegroundWindow auf, kann jedoch
        // wegen der Einschränkung (Kapitel 6) fehlschlagen, daher explizit
        // auch aufrufen, jetzt, da AllowSetForegroundWindow bereits gewährt wurde
        var hwnd = new System.Windows.Interop.WindowInteropHelper(window).Handle;
        SetForegroundWindow(hwnd);

        if (args.Length > 0)
        {
            // args[0] als zu öffnenden Dateipfad behandeln, oder andere app-spezifische Verarbeitung
        }
    }

    private sealed record ActivateRequest(int Version, string[] Args);
}

Drei erwähnenswerte Designentscheidungen:

  • Wählen Sie einen an Global\ angehängten Benutzerbezeichner, der sich im Lauf der Zeit nicht ändert. Der von WindowsIdentity.GetCurrent().User zurückgegebene SecurityIdentifier ist, anders als ein Benutzername, von Umbenennungen unbeeinflusst, und ToString() liefert eine Zeichenkette im Format S-1-5-21-....11 Den Benutzernamen direkt einzubetten kann zu dem Unfall führen, dass die Mehrfachstart-Verhinderung nach einer Kontoumbenennung oder einer Domänenmigration nicht mehr funktioniert.
  • Den Mutex explizit freigeben und den Pipe-Server als Teil des Herunterfahrprozesses der App stoppen. Im obigen Beispiel rufen wir ReleaseMutex in einem finally-Block auf, aber in einer echten App sollten Sie auch die Schleife des Pipe-Servers mithilfe eines Abbruch-Tokens als Teil der Fensterschließlogik stoppen.
  • Ein version-Feld von Anfang an einbeziehen. Es besteht eine reelle Chance, dass Sie das Format der Startargumente künftig ändern. Eine „unbekannte Versionen werden ignoriert“-Prüfung von Anfang an einzubauen, lässt Sie auch auf einem Rechner, auf dem noch eine ältere Version der ausführbaren Datei vorhanden ist, sicher verhalten.

10. Zusammenfassung

Das Verhindern von Mehrfachstarts einer Windows-App wirkt einfach, wenn man nur die wenigen Zeilen von new Mutex(true, name, out createdNew) betrachtet. Um es aber in der Produktion ohne Vorfälle funktionieren zu lassen, braucht man ein vollständiges Verständnis des umgebenden Wissens: den Unterschied in der Sitzungssichtbarkeit zwischen Global\ und Local\, die für Mutex einzigartige Thread-Besitzeinschränkung und den Umgang mit AbandonedMutexException, sowie ein Aktivierungsdesign, das die Einschränkungen von SetForegroundWindow berücksichtigt.

Zur Implementierungsreihenfolge: Entscheiden Sie zunächst, „auf welcher Einheit (Sitzung, Benutzer oder Rechner) Sie Mehrfachstarts verhindern möchten“ (Kapitel 5), und richten Sie den Namensraum des Mutex sowie den Geltungsbereich der Benachrichtigungspipe entsprechend aus. Verwenden Sie von dort aus die Standardtechnik, über AllowSetForegroundWindow Berechtigung zu delegieren, um die vorhandene Instanz nach vorne zu holen, und akzeptieren Sie, dass Sie sich in Fällen, in denen das dennoch fehlschlägt, mit einer Benachrichtigung statt dem erzwungenen Nach-vorne-Holen des Fensters begnügen – mit dieser Denkweise fällt die Implementierung nicht auseinander. Sind Sie unsicher, ob die Anforderung „eine Instanz pro Benutzer“ oder „eine Instanz für den gesamten Rechner“ lautet, empfehlen wir, zunächst die Betriebsumgebung zu klären (ob RDP mit Konsolennutzung kombiniert wird, ob mehrere Benutzer gleichzeitig angemeldet sind).

Verwandte Artikel

Verwandte Beratungsleistungen

Die KomuraSoft LLC übernimmt Design und Implementierung von Windows-Desktop-Apps – einschließlich Mehrfachstart-Verhinderung und Fenstersteuerung –, die Untersuchung von Problemen, die spezifisch für Remotedesktop-Umgebungen sind, sowie Designprüfungen bestehender Apps.

  1. Microsoft Learn, Mutex Constructor. Dazu, dass createdNew als false zurückkommt, ohne dass eine Ausnahme geworfen wird, wenn ein benannter Mutex bereits existiert, und dazu, dass der anfängliche Besitz durch initiallyOwned sich nur auswirkt, wenn createdNew true ist.  2 3

  2. Microsoft Learn, Mutex Class. Dazu, dass ein benannter Mutex standardmäßig Local\ verwendet, wenn kein Präfix angegeben wird, zum Unterschied in der Sichtbarkeit über Terminaldienste-Sitzungen hinweg zwischen Global\/Local\, sowie dazu, dass Mutex den Besitz pro Thread erzwingt (anders als andere Synchronisierungsobjekte).  2 3

  3. Microsoft Learn, Kernel Object Namespaces. Zur Struktur sitzungsgebundener Namensräume gegenüber dem globalen Namensraum sowie zur Angabe eines Namensraums über die Präfixe Global\/Local.  2

  4. Microsoft Learn, Mutex.ReleaseMutex Method. Dazu, dass der Aufruf von ReleaseMutex von einem Thread, der ihn nicht besitzt, ApplicationException wirft, und dazu, dass ein Mutex verwaist, wenn der Thread beendet wird, ohne ihn freizugeben.  2

  5. Microsoft Learn, AbandonedMutexException Class. Dazu, dass der nächste Thread, der einen verwaisten Mutex erwirbt, eine AbandonedMutexException erhält, und dazu, dass dies anzeigt, dass das Warten selbst erfolgreich war und der Aufrufer Besitz am Mutex erlangt hat.  2

  6. Microsoft Learn, SetForegroundWindow function. Zu den Bedingungen, unter denen ein Prozess das Vordergrundfenster setzen darf, und dazu, dass eine Nichterfüllung nur zu einer blinkenden Taskleistenschaltfläche führt.  2

  7. Microsoft Learn, AllowSetForegroundWindow function. Dazu, dass ein Prozess, der das Vordergrundfenster setzen kann, diese Berechtigung an einen anderen Prozess übergeben kann, und dazu, dass die Angabe von ASFW_ANY sie jedem Prozess gewährt.  2

  8. Microsoft Learn, CreateMutexW function (synchapi.h). Dazu, dass beim Beschränken auf eine einzige Instanz mit einem benannten Mutex ein böswilliger Benutzer zuerst einen Mutex mit demselben Namen erzeugen und den Start der App stören kann, sowie zu Alternativen wie einem Zufallsnamen oder, für Fälle mit einer Instanz pro Benutzer, einer Sperrdatei unter dem Benutzerprofil. 

  9. Microsoft Learn, Mutexes. Dazu, dass benannte System-Mutexe systemweit sichtbar und global sind, weshalb empfohlen wird, sie vom Erzeugungszeitpunkt an mit Zugriffskontrollsicherheit zu schützen, sowie zur Zugriffskontrolle über MutexSecurity. 

  10. Microsoft Learn, PipeOptions Enum. Dazu, dass CurrentUserOnly unter Windows zusätzlich zum Benutzerkonto auch die Erhöhungsstufe validiert. 

  11. Microsoft Learn, WindowsIdentity.User Property. Dazu, dass diese Eigenschaft die Sicherheitskennung (SID) des Benutzers zurückgibt, und dazu, dass eine SID einen Benutzer oder eine Gruppe über alle Windows-NT-Implementierungen hinweg eindeutig identifiziert. 

Aktuelle Artikel mit denselben Schlagwörtern führen zu verwandten Themen weiter.

Diese Seiten ordnen den Artikel in einen größeren Leistungs- und Entscheidungskontext ein.

Dieser Artikel ist direkt mit den folgenden Leistungen verbunden.

Häufige Fragen

Fragen, die in Beratungen zu diesem Artikelthema häufig gestellt werden.

Wie verhindere ich in C# Mehrfachstarts einer Windows-App?
Verwenden Sie einen benannten Mutex: Rufen Sie new Mutex(true, name, out bool createdNew) auf und verzweigen Sie anhand von createdNew. Existiert bereits ein Mutex mit demselben Namen, wird keine Ausnahme geworfen – createdNew kommt einfach als false zurück und teilt Ihnen mit, dass bereits eine andere Instanz läuft. Betten Sie eine produktspezifische GUID in den Namen ein, damit es nicht mit Apps anderer Anbieter kollidiert, und geben Sie den Mutex beim normalen Beenden explizit in einem finally-Block frei, damit der nächste Start nicht auf eine unnötige AbandonedMutexException trifft.
Warum funktioniert meine Einzelinstanz-Prüfung nicht über Remotedesktop?
Ein benannter Mutex ohne Präfix wird standardmäßig im sitzungsgebundenen Namensraum Local\ erzeugt, sodass jede Remotedesktop-Sitzung ihren eigenen separaten Mutex erhält und die Mehrfachstart-Verhinderung pro Sitzung unabhängig arbeitet. Möchten Sie eine einzige Instanz über Sitzungen hinweg, ist das Präfix Global\ zwingend erforderlich. Beachten Sie, dass Global\ allein von jedem Benutzer auf dem Rechner gemeinsam genutzt wird, was faktisch eine Instanz für den gesamten Rechner ergibt; für eine Instanz pro Benutzer über dessen Sitzungen hinweg kombinieren Sie Global\ mit einem benutzerspezifischen Bezeichner wie der in den Namen eingebetteten SID.
Warum schafft es SetForegroundWindow nicht, mein vorhandenes Fenster nach vorne zu holen?
Windows schränkt ein, welche Prozesse das Vordergrundfenster setzen dürfen – sofern der Aufrufer nicht der Vordergrundprozess ist, von diesem gestartet wurde, das jüngste Eingabeereignis erhalten hat oder ein paar andere Bedingungen zutreffen, lässt SetForegroundWindow lediglich die Taskleistenschaltfläche blinken. Die vorhandene Hintergrundinstanz erfüllt typischerweise keine davon. Die Standardtechnik nutzt eine Asymmetrie aus: Der zweite Prozess, den der Benutzer gerade per Doppelklick gestartet hat, besitzt meist die Vordergrundberechtigung, sodass er AllowSetForegroundWindow(ASFW_ANY) aufruft, um diese Berechtigung zu übertragen, bevor er die vorhandene Instanz auffordert, sich selbst zu aktivieren – wodurch deren SetForegroundWindow-Aufruf gelingt.
Was bedeutet AbandonedMutexException und wie sollte ich damit umgehen?
Es bedeutet, dass der Thread, dem der Mutex gehörte, beendet wurde, ohne ihn freizugeben – etwa weil der Prozess abgestürzt ist. Die Ausnahme zeigt tatsächlich an, dass das Warten selbst erfolgreich war und der Aufrufer nun Besitzer des Mutex ist. Die korrekte Behandlung besteht nicht darin, sie stillschweigend zu schlucken, sondern die Konsistenz des Zustands zu prüfen, den der Mutex schützte, da der vorige Besitzer ihn inkonsistent hinterlassen haben könnte, und dann entweder nach Verifikation fortzufahren oder ihn sicher neu zu initialisieren. Für reine Starterkennung kommt das selten vor, da die erste Instanz den Mutex bis zum Prozessende hält.

Autorenprofil

Profilseite des Artikelautors.

Go Komura

Geschäftsführer von KomuraSoft LLC

Spezialisiert auf Windows-Softwareentwicklung, technische Beratung und Fehleranalyse, insbesondere bei bestehenden Systemen und schwer reproduzierbaren Störungen.

Zurück zum Blog