Verhindern von Blockaden in Windows-Anwendungen

Betroffene Plattformen

Clients – Windows 7
Server – Windows Server 2008 R2

BESCHREIBUNG

Hängt – Benutzerperspektive

Benutzer mögen reaktionsfähige Anwendungen. Wenn sie auf ein Menü klicken, soll die Anwendung sofort reagieren, auch wenn sie gerade ihre Arbeit druckt. Wenn sie ein langwieriges Dokument in ihrem bevorzugten Textverarbeitungsprogramm speichern, möchten sie die Eingabe fortsetzen, während sich der Datenträger noch dreht. Benutzer werden ziemlich schnell ungeduldig, wenn die Anwendung nicht rechtzeitig auf ihre Eingaben 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 der Benutzerrecherche wissen wir jedoch, dass Benutzer nach nur wenigen Sekunden des Nicht reagierens verärgert und frustriert werden. Nach fünf Sekunden wird versucht, eine hängende Anwendung zu beenden. Neben Abstürzen sind Hängen von Anwendungen die häufigste Ursache für Benutzerunterbrechungen bei der Arbeit mit Win32-Anwendungen.

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

Hängt – Betriebssystemperspektive

Wenn eine Anwendung (genauer gesagt ein Thread) ein Fenster auf dem Desktop erstellt, wird ein impliziter Vertrag mit dem Desktopfenster-Manager (DWM) abgeschlossen, um Fenstermeldungen zeitnah zu verarbeiten. Der DWM stellt Nachrichten (Tastatur-/Mauseingaben und Nachrichten aus anderen Fenstern sowie sich selbst) in die threadspezifische Nachrichtenwarteschlange ein. Der Thread ruft diese Nachrichten ab und verteilt sie über seine Nachrichtenwarteschlange. Wenn der Thread die Warteschlange nicht durch Aufrufen von GetMessage() bedient, werden Nachrichten nicht verarbeitet, und das Fenster hängt: Es kann weder neu gezeichnet noch Eingaben vom Benutzer akzeptiert werden. Das Betriebssystem erkennt diesen Zustand durch Anfügen eines Timers an ausstehende Nachrichten in der Nachrichtenwarteschlange. Wenn eine Nachricht nicht innerhalb von 5 Sekunden abgerufen wurde, deklariert die DWM das Aufhängen des Fensters. Sie können diesen bestimmten Fensterstatus über die IsHungAppWindow()-API abfragen.

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

Das gesamte Gespensterlebnis sieht wie folgt aus:

Screenshot: Dialogfeld

Der Desktopfenster-Manager erledigt noch eine letzte Sache. Es ist in Windows-Fehlerberichterstattung integriert, sodass der Benutzer die Anwendung nicht nur schließen und optional neu starten kann, sondern auch wertvolle Debugdaten an Microsoft zurücksenden kann. Sie können diese Hangdaten für Ihre eigenen Anwendungen erhalten, indem Sie sich auf der Winqual-Website registrieren.

Windows 7 hat dieser Benutzeroberfläche ein neues Feature hinzugefügt. Das Betriebssystem analysiert die hängende Anwendung und gibt dem Benutzer unter bestimmten Umständen die Möglichkeit, einen Blockierungsvorgang abzubrechen und die Anwendung wieder reagieren zu lassen. Die aktuelle Implementierung unterstützt das Absagen 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 Benutzeroberfläche für die Wiederherstellung von Hängen zu integrieren und die verfügbaren Daten optimal zu nutzen:

  • Stellen Sie sicher, dass sich Ihre Anwendung für den Neustart und die Wiederherstellung registriert, sodass der Benutzer so problemlos wie möglich hängen bleibt. Eine ordnungsgemäß registrierte Anwendung kann automatisch neu gestartet werden, wobei die meisten nicht gespeicherten Daten intakt sind. Dies funktioniert sowohl bei Hängenbleiben der Anwendung als auch bei Abstürzen.
  • Rufen Sie Frequenzinformationen sowie Debugdaten für Ihre abgehängten und abgestürzten Anwendungen von der Winqual-Website ab. Sie können diese Informationen auch während der Betaversion verwenden, um Ihren Code zu verbessern. Eine kurze Übersicht finden Sie unter Einführung in Windows-Fehlerberichterstattung.
  • Sie können das Ghosting-Feature in Ihrer Anwendung über einen Aufruf von DisableProcessWindowsGhosting () deaktivieren. Dies verhindert jedoch, dass der durchschnittliche Benutzer eine hängende Anwendung schließt und neu startet, und endet häufig mit einem Neustart.

Hängt – Entwicklerperspektive

Das Betriebssystem definiert eine hängende Anwendung als UI-Thread, der nachrichten seit mindestens 5 Sekunden nicht verarbeitet hat. Offensichtliche Fehler verursachen einige Hängen, z. B. ein Thread, der auf ein Ereignis wartet, das nie signalisiert wird, und zwei Threads, die jeweils eine Sperre halten und versuchen, die anderen abzurufen. Sie können diese Fehler ohne zu viel Aufwand beheben. Viele Hänger sind jedoch nicht so eindeutig. Ja, der UI-Thread ruft keine Nachrichten ab. Er ist aber auch mit anderen "wichtigen" Aufgaben beschäftigt und wird schließlich zur Verarbeitung von Nachrichten zurückkehren.

Der Benutzer nimmt dies jedoch als Fehler wahr. Der Entwurf sollte den Erwartungen des Benutzers entsprechen. Wenn der Entwurf der Anwendung zu einer nicht reagierenden Anwendung führt, muss sich der Entwurf ändern. Schließlich, und dies ist wichtig, kann nicht wie ein Codefehler behoben werden. Es erfordert Vorabarbeit während der Entwurfsphase. Der Versuch, die vorhandene Codebasis einer Anwendung nachzurüsten, um die Benutzeroberfläche reaktionsfähiger zu gestalten, ist häufig zu teuer. Die folgenden Entwurfsrichtlinien können hilfreich sein.

  • Machen Sie die Reaktionsfähigkeit der Benutzeroberfläche zu einer obersten Anforderung; 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 vorgänge im Hintergrund abgeschlossen werden können; Bereitstellen einer geeigneten Fortschritts-Benutzeroberfläche bei Bedarf

Screenshot: Dialogfeld

  • Lang andauernde oder blockierende Vorgänge als Hintergrundaufgaben in die Warteschlange stellen (dies erfordert einen gut durchdachten Messagingmechanismus, um den UI-Thread zu informieren, wenn die Arbeit abgeschlossen ist)
  • Halten Sie den Code für UI-Threads einfach. So viele blockierende API-Aufrufe wie möglich entfernen
  • Fenster und Dialogfelder nur anzeigen, wenn sie bereit und vollständig betriebsbereit sind. Wenn im Dialogfeld Informationen angezeigt werden müssen, die für die Berechnung zu ressourcenintensiv sind, zeigen Sie zuerst einige generische Informationen an, und aktualisieren Sie sie sofort, wenn mehr Daten verfügbar sind. Ein gutes Beispiel ist das Dialogfeld mit den Ordnereigenschaften von Windows Explorer. Es muss die Gesamtgröße des Ordners anzeigen, Informationen, die nicht ohne weiteres im Dateisystem verfügbar sind. Das Dialogfeld wird sofort angezeigt, und das Feld "Größe" wird von einem Workerthread aktualisiert:

Screenshot: Seite

Leider gibt es keine einfache Möglichkeit, eine reaktionsfähige Anwendung zu entwerfen und zu schreiben. Windows bietet kein einfaches asynchrones Framework, das eine einfache Planung von blockierenden oder lang andauernden Vorgängen ermöglicht. In den folgenden Abschnitten werden einige der bewährten Methoden zur Verhinderung von Hängen vorgestellt und einige der häufigen Fallstricke hervorgehoben.

Bewährte Methoden

Halten Sie den UI-Thread einfach

Die Hauptverantwortung des UI-Threads ist das Abrufen und Senden von Nachrichten. Jede andere Art von Arbeit birgt das Risiko, dass die Fenster hängen, die diesem Thread gehören.

Gehen Sie wie folgt vor:

  • Verschieben ressourcenintensiver oder ungebundener Algorithmen, die zu zeitintensiven Vorgängen führen, in Workerthreads
  • Identifizieren Sie so viele blockierende Funktionsaufrufe wie möglich, und versuchen Sie, sie in Workerthreads zu verschieben. Jeder Aufruf einer Funktion in eine andere DLL sollte verdächtig sein
  • Nehmen Sie zusätzlichen Aufwand vor, um alle Datei-E/A- und Netzwerk-API-Aufrufe aus Ihrem Workerthread zu entfernen. Diese Funktionen können für viele Sekunden blockiert werden, wenn nicht minutenlang. Wenn Sie E/A-Vorgänge im UI-Thread durchführen müssen, sollten Sie die Verwendung asynchroner E/A-Vorgänge in Betracht ziehen.
  • Beachten Sie, dass Ihr UI-Thread auch alle STA-COM-Server (Single-Threaded Apartment) verwaltet, die von Ihrem Prozess gehostet werden. Wenn Sie einen blockierenden Anruf tätigen, reagieren diese COM-Server nicht mehr, bis Sie die Nachrichtenwarteschlange erneut warten.

Vermeiden Sie Folgendes:

  • Warten Sie über einen sehr kurzen Zeitraum auf ein beliebiges Kernelobjekt (z. B. Event oder Mutex). Wenn Sie überhaupt warten müssen, erwägen Sie die Verwendung von MsgWaitForMultipleObjects(), wodurch die Blockierung aufgehoben wird, wenn eine neue Nachricht eintrifft.
  • Teilen Sie die Fensternachrichtenwarteschlange eines Threads mit einem anderen Thread, indem Sie die AttachThreadInput()-Funktion verwenden. Es ist nicht nur äußerst schwierig, den Zugriff auf die Warteschlange ordnungsgemäß zu synchronisieren, es kann auch verhindern, dass das Windows-Betriebssystem ein hängendes Fenster ordnungsgemäß erkennt.
  • Verwenden Sie TerminateThread() für einen Ihrer Workerthreads. Wenn Ein Thread auf diese Weise beendet wird, kann er keine Sperren freigeben oder Ereignisse signalisieren 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 zur Reaktionsfähigkeit folgt.
  • Machen Sie jede Art von blockierenden Broadcastanruf; SendMessage(HWND_BROADCAST) macht Sie jeder falsch geschriebenen Anwendung ausgeliefert, die derzeit ausgeführt wird.

Implementieren von asynchronen Mustern

Das Entfernen von vorgängen mit langer Ausführungsdauer oder blockierenden Vorgängen aus dem UI-Thread erfordert die Implementierung eines asynchronen Frameworks, das das Auslagern dieser Vorgänge an Workerthreads ermöglicht.

Gehen Sie wie folgt vor:

  • Verwenden Sie asynchrone Fensternachrichten-APIs in Ihrem UI-Thread, insbesondere indem Sie SendMessage durch einen seiner nicht blockierenden Peers ersetzen: PostMessage, SendNotifyMessage oder SendMessageCallback
  • Verwenden Sie Hintergrundthreads, um aufgaben mit langer Ausführungsdauer oder blockierenden Aufgaben auszuführen. Verwenden der neuen Threadpool-API zum Implementieren Ihrer Workerthreads
  • Stellen Sie Abbruchunterstützung für lang andauernde Hintergrundaufgaben bereit. Verwenden Sie zum Blockieren von E/A-Vorgängen den E/A-Abbruch, aber nur als letztes Mittel. Es ist nicht einfach, den "richtigen" Vorgang abzubrechen.
  • 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 macht Ihre Anwendung reaktionsfähiger. Die Verwendung mehrerer Sperren erhöht jedoch auch die Wahrscheinlichkeit, dass diese Sperren in verschiedenen Reihenfolgen erworben werden und Ihre Threads zu einem Deadlock führen. Wenn zwei Threads jeweils eine Sperre enthalten und dann versuchen, die Sperre des anderen Threads abzurufen, bilden ihre Vorgänge eine zirkuläre Wartezeit, die den 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 derselben Reihenfolge abrufen. Es ist jedoch nicht immer einfach, Schlösser in der "richtigen" Reihenfolge zu erwerben. Softwarekomponenten können zusammengestellt werden, aber Sperren können nicht abgerufen werden. Wenn Ihr Code eine andere Komponente aufruft, werden die Sperren dieser Komponente jetzt Teil Ihrer impliziten Sperrreihenfolge – 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, Mutexes 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 aufheben, wenn der Zielthread diesen Aufruf "loslässt". Einige User32-Funktionen (z. B. SendMessage) sowie viele blockierende COM-Aufrufe fallen in diese Kategorie.

Schlimmer noch, das Betriebssystem verfügt über eine eigene interne prozessspezifische Sperre, die manchmal während der Ausführung des Codes gehalten wird. Diese Sperre wird abgerufen, wenn DLLs in den Prozess geladen werden, und wird daher als "Ladesperre" bezeichnet. Die DllMain-Funktion wird immer unter der Ladeprogrammsperre ausgeführt. Wenn Sie Sperren in DllMain erwerben (und dies nicht der Fall ist), müssen Sie die Ladeprogrammsperre als Teil Ihrer Sperrreihenfolge festlegen. Das Aufrufen bestimmter Win32-APIs kann auch die Ladeprogrammsperre in Ihrem Namen erhalten– Funktionen wie LoadLibraryEx, GetModuleHandle und insbesondere CoCreateInstance.

Um all dies zusammenzubinden, sehen Sie sich den folgenden Beispielcode an. Diese Funktion ruft mehrere Synchronisierungsobjekte ab und definiert implizit eine Sperrreihenfolge, was bei der Cursorüberprüfung nicht unbedingt offensichtlich ist. Beim Funktionseintrag ruft der Code einen kritischen Abschnitt ab und gibt ihn erst wieder frei, wenn die Funktion beendet wird, wodurch er zum obersten Knoten in unserer Sperrhierarchie wird. Der Code ruft dann die Win32-Funktion LoadIcon() auf, die im Cover möglicherweise in den Betriebssystemladeprogramm aufruft, um diese Binärdatei zu laden. Dieser Vorgang würde die Ladeprogrammsperre abrufen, die nun ebenfalls Teil dieser Sperrhierarchie wird (stellen Sie sicher, dass die DllMain-Funktion nicht die g_cs Sperre abruft). Als Nächstes ruft der Code SendMessage() auf, einen blockierenden threadübergreifenden Vorgang, der nur zurückgegeben wird, wenn der UI-Thread antwortet. Stellen Sie auch hier sicher, dass der UI-Thread nie g_cs abruft.

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;  
}  

Wenn wir diesen Code betrachten, ist klar, dass wir implizit g_cs die Sperre der obersten Ebene in unserer Sperrhierarchie vorgenommen haben, auch wenn wir nur den Zugriff auf die Klassenmembervariablen synchronisieren wollten.

Gehen Sie wie folgt vor:

  • Entwerfen Sie eine Sperrhierarchie, und befolgen Sie sie. Fügen Sie alle erforderlichen Sperren hinzu. Es gibt viel mehr Synchronisierungsgrundtypen als nur Mutex und CriticalSections; sie alle müssen einbezogen werden. Schließen Sie die Ladeprogrammsperre in Ihre Hierarchie ein, wenn Sie sperren in DllMain()
  • Vereinbaren Sie das Sperrprotokoll mit Ihren Abhängigkeiten. Jeder Code, den Ihre Anwendung aufruft oder die Anwendung aufrufen kann, muss dieselbe Sperrhierarchie verwenden.
  • Sperren von Datenstrukturen funktioniert nicht. Verschieben Sie Sperreskäufe von Funktionseinstiegspunkten weg, und schützen Sie nur den Datenzugriff mit Sperren. Wenn weniger Code unter einer Sperre ausgeführt wird, besteht eine geringere Chance für Deadlocks.
  • Analysieren Sie Sperreskäufe und -releases in Ihrem Fehlerbehandlungscode. Häufig wird die Sperrhierarchie bei der Wiederherstellung nach einer Fehlerbedingung vergessen.
  • Ersetzen Sie geschachtelte Sperren durch Verweisindikatoren, da sie kein Deadlock sind. Individuell gesperrte Elemente in Listen und Tabellen sind gute Kandidaten
  • Seien Sie vorsichtig, wenn Sie auf ein Threadhandle aus einer DLL warten. Gehen Sie immer davon aus, dass Ihr Code unter der Ladesperre aufgerufen werden könnte. Es ist besser, auf Ihre Ressourcen zu verweisen und den Workerthread seine eigene Bereinigung durchführen zu lassen (und dann FreeLibraryAndExitThread zum sauberen Beenden zu verwenden).
  • Verwenden Sie die Wartekette-Traversal-API, wenn Sie Ihre eigenen Deadlocks diagnostizieren möchten.

Vermeiden Sie Folgendes:

  • Führen Sie in Ihrer DllMain()-Funktion alles andere als eine sehr einfache Initialisierung aus. Weitere Informationen finden Sie unter DllMain-Rückruffunktion. Rufen Sie insbesondere LoadLibraryEx oder CoCreateInstance nicht auf.
  • Schreiben Sie Ihre eigenen Sperrgrundtypen. Benutzerdefinierter Synchronisierungscode kann leicht zu geringfügigen Fehlern in Ihre Codebasis führen. Verwenden Sie stattdessen die umfangreiche Auswahl von Betriebssystemsynchronisierungsobjekten.
  • Erledigen Sie alle Arbeiten in den Konstruktoren und Destruktoren für globale Variablen, sie werden unter der Ladeprogrammsperre ausgeführt.

Vorsicht bei Ausnahmen

Ausnahmen ermöglichen die Trennung von normalem Programmablauf und Fehlerbehandlung. Aufgrund dieser Trennung kann es schwierig sein, den genauen Zustand des Programms vor der Ausnahme zu kennen, und der Ausnahmehandler kann wichtige Schritte beim Wiederherstellen eines gültigen Zustands übersehen. Dies gilt insbesondere für Sperrenkä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 Variable "buffer" führt gelegentlich zu einer Zugriffsverletzung (AV). Diese AV wird vom nativen Ausnahmehandler erfasst, aber es gibt keine einfache Möglichkeit, zu bestimmen, ob der kritische Abschnitt zum Zeitpunkt der Ausnahme bereits abgerufen wurde (der 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;  
}  

Gehen Sie wie folgt vor:

  • Entfernen Sie nach Möglichkeit __try/__except. SetUnhandledExceptionFilter nicht verwenden
  • Umschließen Sie Ihre Sperren in benutzerdefinierte auto_ptr-ähnlichen Vorlagen, wenn Sie C++-Ausnahmen verwenden. Die Sperre sollte im Destruktor aufgehoben werden. Für native Ausnahmen lassen Sie die Sperren in Ihrer __finally-Anweisung frei.
  • Gehen Sie vorsichtig mit dem Code vor, der in einem nativen Ausnahmehandler ausgeführt wird. Die Ausnahme hat möglicherweise viele Sperren verloren, sodass Ihr Handler keine abrufen sollte.

Vermeiden Sie Folgendes:

  • Behandeln Sie native Ausnahmen, wenn dies nicht erforderlich 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 Windows-Fehlerberichterstattung verwenden.
  • Verwenden Sie C++-Ausnahmen mit beliebigem Benutzeroberflächencode (user32). Eine Ausnahme, die in einem Rückruf ausgelöst wird, wird durch vom Betriebssystem bereitgestellte C-Codeebenen durchlaufen. Dieser Code kennt keine C++-Derollsemantik.