Verhindern von Blockaden in Windows-Anwendungen

Betroffene Plattformen

Clients – Windows 7
Server – Windows Server 2008 R2

BESCHREIBUNG

Hängt – Benutzerperspektive

Benutzer möchten reaktionsschnelle Anwendungen. Wenn sie auf ein Menü klicken, soll die Anwendung sofort reagieren, auch wenn sie gerade ihre Arbeit druckt. Wenn sie ein langes Dokument in ihrem bevorzugten Textprozessor speichern, möchten sie die Eingabe fortsetzen, während sich der Datenträger noch im Drehen befindet. Benutzer werden schnell ungeduldig, wenn die Anwendung nicht rechtzeitig auf ihre Eingabe reagiert.

Ein Programmierer erkennt möglicherweise viele legitime Gründe dafür, dass eine Anwendung nicht sofort auf Benutzereingaben reagiert. Die Anwendung ist möglicherweise damit beschäftigt, einige Daten neu zu berechnen oder einfach auf den Abschluss der Datenträger-E/A zu warten. Aus Benutzerforschungen wissen wir jedoch, dass Benutzer nach wenigen Sekunden nicht reagierender Benutzer verärgert und frustriert werden. Nach 5 Sekunden wird versucht, eine hängende Anwendung zu beenden. Neben Abstürzen sind abstürzende Anwendungen die häufigste Ursache für Benutzerunterbrechungen bei der Arbeit mit Win32-Anwendungen.

Es gibt viele verschiedene Ursachen für hängende Anwendungen, und nicht alle davon manifestieren sich in einer nicht reagierenden Benutzeroberfläche. Eine nicht reagierende Benutzeroberfläche ist jedoch eine der häufigsten Hängen, und dieses Szenario erhält derzeit die meisten Betriebssystemunterstützung sowohl für die Erkennung als auch für die Wiederherstellung. Windows erkennt, sammelt Debuginformationen und beendet oder startet hängende Anwendungen optional neu. Andernfalls muss der Benutzer den Computer möglicherweise neu starten, um eine hängende Anwendung wiederhergestellt zu haben.

Hängt – Perspektive des Betriebssystems

Wenn eine Anwendung (oder genauer gesagt ein Thread) ein Fenster auf dem Desktop erstellt, geht sie einen impliziten Vertrag mit dem Desktopfenster-Manager (DWM) ein, um Fenstermeldungen rechtzeitig zu verarbeiten. Das DWM veröffentlicht Nachrichten (Tastatur-/Mauseingaben und Nachrichten aus anderen Fenstern sowie sich selbst) in die threadspezifische Nachrichtenwarteschlange. Der Thread ruft diese Nachrichten über seine Nachrichtenwarteschlange ab und gibt sie weiter. Wenn der Thread die Warteschlange nicht durch Aufrufen von GetMessage() verarbeitet, werden Nachrichten nicht verarbeitet, und das Fenster hängt: Es kann weder neu gezeichnet werden noch kann er Eingaben vom Benutzer akzeptieren. Das Betriebssystem erkennt diesen Zustand, indem es einen Timer an ausstehende Nachrichten in der Nachrichtenwarteschlange anfügen. Wenn eine Nachricht nicht innerhalb von 5 Sekunden abgerufen wurde, deklariert das DWM, dass das Fenster hängen ist. Sie können diesen bestimmten Fensterzustand über die IsHungAppWindow()-API abfragen.

Die Erkennung ist nur der erste Schritt. An diesem Punkt kann der Benutzer die Anwendung noch nicht einmal beenden. Wenn Sie auf die Schaltfläche X (Schließen) klicken, würde dies zu einer WM CLOSE-Meldung führen, die wie jede andere Nachricht in der Nachrichtenwarteschlange hängen _ bleibt. Der Desktopfenster-Manager unterstützt Sie dabei, das hängende Fenster nahtlos zu verbergen und dann durch eine "ingespensste" Kopie zu ersetzen, die eine Bitmap des vorherigen Clientbereichs des ursprünglichen Fensters zeigt (und der Titelleiste "Nicht reagierend" hinzufügung). Solange der Thread des ursprünglichen Fensters keine Nachrichten abruft, verwaltet das DWM beide Fenster gleichzeitig, ermöglicht dem Benutzer jedoch die Interaktion nur mit der ingesennten Kopie. Mithilfe dieses insinktiven Fensters kann der Benutzer die nicht reagierende Anwendung nur verschieben, minimieren und – was am wichtigsten ist – schließen, aber nicht ihren internen Zustand ändern.

Die gesamte In ghost-Erfahrung sieht wie die folgenden aus:

Screenshot: Dialogfeld "Editor reagiert nicht"

Die Desktopfenster-Manager führt eine letzte Sache aus: Sie ist in Windows-Fehlerberichterstattung integriert, sodass der Benutzer die Anwendung nicht nur schließt und optional neu startet, sondern auch wertvolle Debugdaten an Microsoft zurücksingen kann. Sie können diese hängenden Daten für Ihre eigenen Anwendungen erhalten, indem Sie sich auf der Winqual-Website anmelden.

Windows 7 wurde dieser Funktion ein neues Feature hinzugefügt. Das Betriebssystem analysiert die hängende Anwendung und gibt dem Benutzer unter bestimmten Umständen die Möglichkeit, einen blockierenden Vorgang abzubricht und die Anwendung wieder reaktionsfähig zu machen. Die aktuelle Implementierung unterstützt den Abbruch von blockierenden Socketaufrufen. Weitere Vorgänge können in zukünftigen Releases vom Benutzer abgebrochen werden.

Führen Sie die folgenden Schritte aus, um Ihre Anwendung in die Wiederherstellung mit Hängen zu integrieren und die verfügbaren Daten so gut wie möglich zu nutzen:

  • Stellen Sie sicher, dass sich Ihre Anwendung für neustart- und wiederherstellungsbasierte Registrierungen registriert, wodurch der Benutzer möglichst unbesorgt bleibt. Eine ordnungsgemäß registrierte Anwendung kann automatisch neu gestartet werden, während die meisten nicht gespeicherten Daten intakt sind. Dies funktioniert sowohl bei hängenden als auch abstürzenden Anwendungen.
  • Von der Winqual-Website erhalten Sie Häufigkeitsinformationen sowie Debugdaten für Ihre hängenden und abgestürzten Anwendungen. Sie können diese Informationen auch während der Betaversion verwenden, um Ihren Code zu verbessern. Unter "Einführung Windows-Fehlerberichterstattung" finden Sie eine kurze Übersicht.
  • Sie können das Ghostingfeature in Ihrer Anwendung über einen Aufruf von DisableProcessWindowsGhosting () deaktivieren. Dadurch wird jedoch verhindert, dass der durchschnittliche Benutzer eine hängende Anwendung schließt und neu startet und häufig mit einem Neustart endet.

Hängt – Entwicklerperspektive

Das Betriebssystem definiert eine Anwendung hängt als UI-Thread, der Nachrichten mindestens 5 Sekunden lang nicht verarbeitet hat. Offensichtliche Fehler verursachen einige Hängen, z. B. einen Thread, der auf ein Ereignis wartet, das nie signalisiert wird, und zwei Threads, die jeweils eine Sperre halten und versuchen, die anderen zu erhalten. Sie können diese Fehler ohne zu großen Aufwand beheben. Viele Hängen sind jedoch nicht so klar. Ja, der UI-Thread ruft keine Nachrichten ab. Er ist jedoch gleichermaßen damit beschäftigt, andere "wichtige" Arbeit zu verarbeiten, und wird schließlich zur Verarbeitung von Nachrichten zurückkommen.

Der Benutzer sieht dies jedoch als Fehler. Der Entwurf sollte den Erwartungen des Benutzers entsprechen. Wenn der Entwurf der Anwendung zu einer nicht reagierenden Anwendung führt, muss der Entwurf geändert werden. Und dies ist wichtig: Nicht reagierende Fehler können nicht wie ein Codefehler behoben werden. Während der Entwurfsphase ist eine Vorabarbeit erforderlich. Der Versuch, die vorhandene Codebasis einer Anwendung zu übertünten, um die Benutzeroberfläche reaktionsfähiger zu machen, ist häufig zu teuer. Die folgenden Entwurfsrichtlinien können helfen.

  • Machen Sie die Reaktionsfähigkeit der Benutzeroberfläche zu einer Anforderung der obersten Ebene. Der Benutzer sollte immer die Kontrolle über Ihre Anwendung haben.
  • Stellen Sie sicher, dass Benutzer Vorgänge abbrechen können, deren Abschluss länger als eine Sekunde dauert, und/oder dass Vorgänge im Hintergrund abgeschlossen werden können. Stellen Sie bei Bedarf eine entsprechende Statusbenutzeroberfläche zur Verfügung.

Screenshot: Dialogfeld "Elemente kopieren"

  • Warteschlangenvorgänge mit langer Ausführung oder blockierende Vorgänge als Hintergrundaufgaben (dies erfordert einen gut durchdachten Messagingmechanismus, um den UI-Thread zu informieren, wenn die Arbeit abgeschlossen ist).
  • Halten Sie den Code für Benutzeroberflächenthreads einfach. So viele blockierende API-Aufrufe wie möglich entfernen
  • Fenster und Dialogfelder werden nur angezeigt, wenn sie bereit und voll funktionsfähig sind. Wenn im Dialogfeld Informationen angezeigt werden müssen, die zu ressourcenintensiv für die Berechnung sind, zeigen Sie zuerst einige allgemeine Informationen an, und aktualisieren Sie sie sofort, wenn mehr Daten verfügbar sind. Ein gutes Beispiel ist das Dialogfeld mit den Ordnereigenschaften aus Windows Explorer. Sie muss die Gesamtgröße des Ordners anzeigen. Informationen, die im Dateisystem nicht sofort verfügbar sind. Das Dialogfeld wird sofort angezeigt, und das Feld "Größe" wird von einem Arbeitsthread aktualisiert:

Screenshot der Seite "Allgemein" von Windows Eigenschaften mit eingekreisten Texten "Größe", "Größe auf Datenträger" und "Enthält".

Leider gibt es keine einfache Möglichkeit, eine reaktionsfähige Anwendung zu entwerfen und zu schreiben. Windows stellt kein einfaches asynchrones Framework zur Verfügung, das eine einfache Planung von blockierenden oder lang ausgeführten Vorgängen ermöglicht. In den folgenden Abschnitten werden einige der bewährten Methoden zum Verhindern von Hängen und hervorheben einige der häufigsten Fallstricke erläutert.

Bewährte Methoden

Benutzeroberflächenthread einfach halten

Die Primäre Aufgabe des UI-Threads ist das Abrufen und Senden von Nachrichten. Jede andere Art von Arbeit bringt das Risiko mit sich, die Fenster, die sich im Besitz dieses Threads befinden, zu hängen.

Tun:

  • Verschieben ressourcenintensiver oder ungebundener Algorithmen, die zu Zeitintensiven Vorgängen in Arbeitsthreads führen
  • Identifizieren Sie so viele blockierende Funktionsaufrufe wie möglich, und versuchen Sie, sie in Arbeitsthreads zu verschieben. Jede Funktion, die eine andere DLL aufruft, sollte verdächtig sein.
  • Machen Sie zusätzlichen Aufwand, um alle Datei-E/A- und Netzwerk-API-Aufrufe aus Ihrem Arbeitsthread zu entfernen. Diese Funktionen können für viele Sekunden blockiert werden, wenn nicht für Minuten. Wenn Sie E/A-Vorgänge im UI-Thread ausführen müssen, sollten Sie die Verwendung von asynchronen E/A-Vorgängen in Erwägung ziehen.
  • Beachten Sie, dass Ihr UI-Thread auch alle COM-Server mit Singlethread-Apartment (STA) bedient, die von Ihrem Prozess gehostet werden. Wenn Sie einen blockierenden Aufruf senden, reagieren diese COM-Server erst, wenn Sie die Nachrichtenwarteschlange erneut warten.

Tue nicht:

  • Warten Sie auf ein Kernelobjekt (z. B. Ereignis oder Mutex) für mehr als einen sehr kurzen Zeitraum. Wenn Sie überhaupt warten müssen, erwägen Sie die Verwendung von MsgWaitForMultipleObjects(), wodurch die Blockierung aufgehoben wird, wenn eine neue Nachricht eintrifft.
  • Geben Sie die Fensternachrichtenwarteschlange eines Threads mithilfe der AttachThreadInput()-Funktion für einen anderen Thread weiter. Es ist nicht nur äußerst schwierig, den Zugriff auf die Warteschlange ordnungsgemäß zu synchronisieren, sondern kann auch verhindern, dass das Windows betriebssystem ein nicht reagiertes Fenster ordnungsgemäß erkennt.
  • Verwenden Sie TerminateThread() für jeden Ihrer Arbeitsthreads. Das Beenden eines Threads auf diese Weise lässt nicht zu, dass sperren oder Ereignisse signalisiert werden, und kann leicht zu verwaisten Synchronisierungsobjekten führen.
  • Rufen Sie einen beliebigen "unbekannten" Code aus Ihrem UI-Thread auf. Dies gilt insbesondere, wenn Ihre Anwendung über ein Erweiterbarkeitsmodell verfügt. Es gibt keine Garantie dafür, dass Der Code von Drittanbietern Ihren Richtlinien für die Reaktionsfähigkeit entspricht.
  • Nehmen Sie eine beliebige Art von blockierenden Broadcastaufruf vor. SendMessage(HWND BROADCAST) versetzt Sie in den Blick jeder nicht _ geschriebenen Anwendung, die derzeit ausgeführt wird.

Implementieren von asynchronen Mustern

Das Entfernen von Vorgängen mit langer Ausführung oder Blockieren aus dem UI-Thread erfordert die Implementierung eines asynchronen Frameworks, das das Ausladen dieser Vorgänge in Arbeitsthreads ermöglicht.

Tun:

  • Verwenden Sie asynchrone Fensternachrichten-APIs in Ihrem UI-Thread, insbesondere durch Ersetzen von SendMessage durch einen seiner nicht blockierenden Peers: PostMessage, SendNotifyMessage oder SendMessageCallback.
  • Verwenden Sie Hintergrundthreads, um Aufgaben mit langer Ausführung oder Blockierung auszuführen. Verwenden der neuen Threadpool-API zum Implementieren Ihrer Arbeitsthreads
  • Bereitstellen von Abbruchunterstützung für Hintergrundaufgaben mit langer Laufzeit. Verwenden Sie zum Blockieren von E/A-Vorgängen den E/A-Abbruch, aber nur als letzten Ausweg. Es ist nicht einfach, den "richtigen" Vorgang abzubricht.
  • Implementieren eines asynchronen Entwurfs für verwalteten Code mithilfe des IAsyncResult-Musters oder mithilfe von Ereignissen

Verwenden von Sperren mit Bedacht

Ihre Anwendung oder DLL benötigt Sperren, um den Zugriff auf ihre internen Datenstrukturen zu synchronisieren. Die Verwendung mehrerer Sperren erhöht die Parallelität und erhöht die Reaktionsfähigkeit Ihrer Anwendung. Durch die Verwendung mehrerer Sperren erhöht sich jedoch auch die Wahrscheinlichkeit, dass diese Sperren in unterschiedlichen Bestellungen verwendet werden und ihre Threads zu einem Deadlock führen. Wenn zwei Threads jeweils eine Sperre halten und dann versuchen, die Sperre des anderen Threads zu erhalten, bilden ihre Vorgänge einen kreisförmigen Wartevorgänge, der jeden Vorwärtsfortschritt für diese Threads blockiert. Sie können diesen Deadlock nur vermeiden, indem Sie sicherstellen, dass alle Threads in der Anwendung immer alle Sperren in der gleichen Reihenfolge erhalten. Es ist jedoch nicht immer einfach, Sperren in der richtigen Reihenfolge zu erhalten. Softwarekomponenten können zusammengestellt werden, Sperrenübernahmen jedoch nicht. Wenn Ihr Code eine andere Komponente aufruft, werden die Sperren dieser Komponente jetzt Teil Ihrer impliziten Sperr reihenfolge – auch wenn Sie keinen Einblick in diese Sperren haben.

Dies wird noch schwieriger, da Sperrvorgänge weit mehr als die üblichen Funktionen für kritische Abschnitte, Mutexe und andere herkömmliche Sperren umfassen. Jeder blockierende Aufruf, der Threadgrenzen überschreitet, verfügt über Synchronisierungseigenschaften, die zu einem Deadlock führen können. Der aufrufende Thread führt einen Vorgang mit "acquire"-Semantik aus und kann die Blockierung erst wieder aufsperren, wenn der Zielthread den Aufruf "freilässt". Einige User32-Funktionen (z. B. SendMessage) sowie viele blockierende COM-Aufrufe fallen in diese Kategorie.

Noch schlechter ist, dass das Betriebssystem über eine eigene interne prozessspezifische Sperre verfügt, die manchmal gehalten wird, während Ihr Code ausgeführt wird. Diese Sperre wird beim Laden von DLLs in den Prozess und daher als "Ladesperre" bezeichnet. Die DllMain-Funktion wird immer unter der Loadersperre ausgeführt. Wenn Sie Sperren in DllMain erhalten (und dies nicht der Fall ist), müssen Sie die Loadersperre als Teil Ihrer Sperr reihenfolge verwenden. Das Aufrufen bestimmter Win32-APIs kann auch die Loadersperre in Ihrem Namen erhalten– Funktionen wie LoadLibraryEx, GetModuleHandle und insbesondere CoCreateInstance.

Sehen Sie sich den folgenden Beispielcode an, um all dies miteinander zu verknüpfen. Diese Funktion übernimmt mehrere Synchronisierungsobjekte und definiert implizit eine Sperr reihenfolge, die bei der Cursorüberprüfung nicht unbedingt offensichtlich ist. Beim Funktionseintrag erhält der Code einen kritischen Abschnitt und gibt ihn erst dann frei, wenn die Funktion beendet wird. Dadurch wird er zum obersten Knoten in unserer Sperrenhierarchie. Der Code ruft dann die Win32-Funktion LoadIcon() auf, die im Untergang möglicherweise das Betriebssystemlader aufruft, um diese Binärdatei zu laden. Dieser Vorgang würde die Loadersperre erhalten, die jetzt auch Teil dieser Sperrhierarchie wird (stellen Sie sicher, dass die DllMain-Funktion die g cs-Sperre _ nicht erhält). Als Nächstes ruft der Code SendMessage() auf, einen blockierenden threadübergreifenden Vorgang, der nur dann zurück gibt, wenn der UI-Thread antwortet. Stellen Sie auch hier sicher, dass der UI-Thread nie g _ cs erhält.

bool foo::bar (char* buffer)  
{  
      EnterCriticalSection(&g_cs);  
      // Get 'new data' icon  
      this.m_Icon = LoadIcon(hInst, MAKEINTRESOURCE(5));  
      // Let UI thread know to update icon SendMessage(hWnd,WM_COMMAND,IDM_ICON,NULL);  
      this.m_Params = GetParams(buffer);  
      LeaveCriticalSection(&g_cs);
      return true;  
}  

Bei diesem Code scheint klar zu sein, dass wir g cs implizit zur Sperre der obersten Ebene in unserer Sperrhierarchie gemacht haben, auch wenn wir nur den Zugriff auf die Klassenmitgliedsvariablen _ synchronisieren wollten.

Tun:

  • Entwerfen Sie eine Sperrhierarchie, und befolgen Sie sie. Fügen Sie alle erforderlichen Sperren hinzu. Es gibt viel mehr Synchronisierungsprimitiven als nur Mutex und CriticalSections. sie müssen alle einbezogen werden. Schließen Sie die Loadersperre in Ihre Hierarchie ein, wenn Sie Sperren in DllMain() verwenden.
  • Stimmen Sie dem Sperrprotokoll mit Ihren Abhängigkeiten zu. Jeder Code, den Ihre Anwendung aufruft oder der Ihre Anwendung aufrufen kann, muss dieselbe Sperrhierarchie verwenden.
  • Sperren Von Datenstrukturen, nicht von Funktionen. Verschieben Sie Sperrkäufe von Funktionseinstiegspunkten weg, und schützen Sie nur den Datenzugriff mit Sperren. Wenn weniger Code unter einer Sperre arbeitet, besteht weniger Wahrscheinlichkeit für Deadlocks.
  • Analysieren Von Sperrenkäufen und -releases in Ihrem Fehlerbehandlungscode. Häufig wird die Sperrhierarchie vergessen, wenn versucht wird, eine Wiederherstellung nach einem Fehlerzustand zu versuchen.
  • Ersetzen Sie geschachtelte Sperren durch Verweiszähler– sie können nicht deadlocken. Einzeln gesperrte Elemente in Listen und Tabellen sind gute Kandidaten
  • Seien Sie vorsichtig, wenn Sie auf ein Threadhand handle aus einer DLL warten. Gehen Sie immer davon aus, dass Ihr Code unter der Loadersperre aufgerufen werden kann. Es ist besser, ihre Ressourcen auf die Verweisanzahl zu verweisen und dem Arbeitsthread eine eigene Bereinigung zu ermöglichen (und dann FreeLibraryAndExitThread zu verwenden, um eine saubere Beendigung zu ermöglichen).
  • Verwenden Sie die Wait Chain Traversal-API, wenn Sie Ihre eigenen Deadlocks diagnostizieren möchten.

Tue nicht:

  • Machen Sie alles andere als eine sehr einfache Initialisierung in Ihrer DllMain()-Funktion. Weitere Informationen finden Sie unter DllMain-Rückruffunktion. Rufen Sie insbesondere LoadLibraryEx oder CoCreateInstance nicht auf.
  • Schreiben Sie ihre eigenen Sperrprimitiven. Benutzerdefinierter Synchronisierungscode kann leicht zu feinen Fehlern in Ihrer Codebasis führen. Verwenden Sie stattdessen die umfangreiche Auswahl von Betriebssystemsynchronisierungsobjekten.
  • Führen Sie jede Arbeit in den Konstruktoren und Destruktoren für globale Variablen aus. Sie werden unter der Ladersperre ausgeführt.

Seien Sie vorsichtig mit Ausnahmen

Ausnahmen ermöglichen die Trennung des normalen Programmablaufs und der Fehlerbehandlung. Aufgrund dieser Trennung kann es schwierig sein, den genauen Zustand des Programms vor der Ausnahme zu kennen, und der Ausnahmehandler kann wichtige Schritte zum Wiederherstellen eines gültigen Zustands übersehen. Dies gilt insbesondere für Sperrenabkäufe, die im Handler freigegeben werden müssen, um zukünftige Deadlocks zu verhindern.

Der folgende Beispielcode veranschaulicht dieses Problem. Der ungebundene Zugriff auf die Puffervariable führt gelegentlich zu einer Zugriffsverletzung (AV). Diese AV wird vom nativen Ausnahmehandler erfasst, kann jedoch nicht einfach feststellen, ob der kritische Abschnitt zum Zeitpunkt der Ausnahme bereits erfasst wurde (die AV hätte sogar irgendwo im EnterCriticalSection-Code stattfinden können).

 BOOL bar (char* buffer)  
{  
   BOOL rc = FALSE;  
   __try {  
      EnterCriticalSection(&cs);  
      while (*buffer++ != '&') ;  
      rc = GetParams(buffer);  
      LeaveCriticalSection(&cs);  
   } __except (EXCEPTION_EXECUTE_HANDLER)  
   {  
      return FALSE;  
   } 
   return rc;  
}  

Tun:

  • Remove _ _ _ _ try/except whenever possible; do not use SetUnhandledExceptionFilter
  • Umschließen Sie Ihre Sperren in _ benutzerdefinierte, automatisch ptr-orientierte Vorlagen, wenn Sie C++-Ausnahmen verwenden. Die Sperre sollte im Destruktor freigegeben werden. Bei nativen Ausnahmen werden die Sperren in Ihrer _ _ finally-Anweisung wieder frei.
  • Seien Sie vorsichtig mit dem Code, der in einem nativen Ausnahmehandler ausgeführt wird. Die Ausnahme hat möglicherweise viele Sperren geleert, sodass ihr Handler keines erhalten sollte.

Tue nicht:

  • Behandeln Sie native Ausnahmen, wenn dies nicht erforderlich ist oder für die Win32-APIs erforderlich ist. Wenn Sie native Ausnahmehandler für die Berichterstellung oder Datenwiederherstellung nach schwerwiegenden Fehlern verwenden, sollten Sie stattdessen den Standardbetriebssystemmechanismus von Windows-Fehlerberichterstattung verwenden.
  • Verwenden Sie C++-Ausnahmen mit beliebigem Benutzeroberflächencode (user32). Eine in einem Rückruf ausgelöste Ausnahme durchfing ebenen von C-Code, der vom Betriebssystem bereitgestellt wird. Dieser Code kennt die C++-Semantik zum Entrollen nicht.