Was „Keine Rückmeldung“ wirklich ist — Wie Windows entscheidet, dass eine App hängt, und wie Sie Apps entwerfen, die das nicht tun
· Go Komura · Windows, Windows-Entwicklung, Fehleruntersuchung, Multithreading, WinForms, WPF, Win32-API, UI-Design
„Die App wird mitten in der Bedienung weiß und zeigt (Keine Rückmeldung).“ „Wir bekommen Tickets, dass sie gelegentlich hängt, aber auf einer Entwicklungsmaschine reproduziert es sich nie.“ — Für Windows-Geschäftsanwendungen ist dieses „Keine Rückmeldung“ eine der häufigsten Beschwerden. Und was überraschend wenig bekannt ist: Das, was die Anzeige „Keine Rückmeldung“ aufstellt, ist nicht die hängende App selbst — es ist das Betriebssystem.
Wie weiß Windows, dass eine App „hängt“? Was ist dieses milchig-weiße Fenster? An Entwickler gerichtet, die Geschäftsanwendungen unter Windows schreiben, und an IT-Personal, das Tickets über hängende Apps entgegennimmt, arbeitet dieser Artikel das Urteil „Keine Rückmeldung“ von den Grundlagen der Nachrichtenschleife durch und ordnet die klassischen Ursachen von Hängern, Entwürfe, die nicht hängen, und ein Verfahren zur Untersuchung des hängenden Moments — alles in Primärquellen verankert.
1. Zuerst das Fazit
- „Keine Rückmeldung“ ist das Urteil des Betriebssystems. Wenn ein Fenster (und der GUI-Thread, dem es gehört) nicht auf Eingabe wartet, nicht in seiner Startsequenz ist und 5 Sekunden lang keine Nachricht abgeholt hat (
PeekMessage), behandelt das Betriebssystem es als ohne Rückmeldung. Das Urteil ist nicht pro Prozess.1 - Das weiß gewordene Fenster ist ein „Geisterfenster“. Das Betriebssystem hat das ursprüngliche Fenster verborgen und eine Attrappe derselben Position, Größe und Erscheinung eingewechselt. Alles, was Sie tun können, ist Verschieben, Minimieren oder Schließen; der Inhalt läuft nicht. Ein Geisterfenster wird nicht angelegt, während ein Debugger verbunden ist.2
- Die Ursache eines Hängers läuft fast immer auf eine Sache hinaus. Der UI-Thread, der die Nachrichtenschleife pumpen sollte, ist auf schwerer Arbeit oder einem Warten blockiert. Synchrones I/O, Netzaufrufe, Sperrwarten und
SendMessageüber Threads hinweg sind die Klassiker.3 - Das Entwurfsprinzip ist „warten oder rechnen Sie nicht auf dem UI-Thread“. Verschieben Sie schwere Arbeit auf einen Worker-Thread (
async/await+Task.Runin C#) und lassen Sie den UI-Thread dem Zeichnen, dem Fortschritt und dem Annehmen von Abbruch gewidmet.4 DoEventsund das manuelle Pumpen der Nachrichtenschleife sind ein Brutplatz für Wiedereintrittsfehler. Die Anzeige „Keine Rückmeldung“ geht weg, aber die Struktur lässt jetzt beliebige Ereignisse mitten in der Arbeit unterbrechen. Trennung, nicht Ausweichen, ist der richtige Ansatz.- Untersuchung beginnt damit, den Zustand im hängenden Moment festzuhalten. Nehmen Sie einen Dump und schauen Sie auf den Stack des UI-Threads, und Sie können fast immer identifizieren, worauf er wartet.
2. Voraussetzung: Windows-Apps werden von Nachrichten angetrieben
Um „Keine Rückmeldung“ zu verstehen, müssen Sie zuerst aufnehmen, dass eine Windows-GUI-App ereignisgetrieben ist. Eine GUI-App holt Eingabe nicht selbst; sie empfängt Nachrichten, die das Betriebssystem zustellt (Maus, Tastatur, Neuzeichenanforderungen, Timer und so weiter), und handelt danach.3
Jeder Thread, der ein Fenster anlegt, hat eine Nachrichtenwarteschlange und führt eine Nachrichtenschleife wie diese aus.
MSG msg;
while (GetMessage(&msg, NULL, 0, 0) > 0) {
TranslateMessage(&msg);
DispatchMessage(&msg); // the window procedure is called
}
GetMessage holt eine Nachricht aus der Warteschlange, und DispatchMessage ruft die Fensterprozedur dieses Fensters auf (die Nachrichtenbehandlungsfunktion). Schaltflächenklickbehandlung, Neuzeichnen und WinForms- oder WPF-Ereignishandler laufen alle, wenn Sie sie herunterkochen, innerhalb einer Iteration dieser Schleife.5
flowchart TB
accTitle: Grundstruktur der Nachrichtenschleife
accDescr: Das Betriebssystem legt Maus, Tastatur und andere Eingabe in die Nachrichtenwarteschlange des Threads; die Schleife des UI-Threads holt sie mit GetMessage, ruft die Fensterprozedur mit DispatchMessage auf und kehrt zum Anfang der Schleife zurück, wenn die Verarbeitung fertig ist
os["Betriebssystem(Eingabe, Neuzeichenanforderungen, Timer)"] --> q["Nachrichtenwarteschlange des Threads"]
q --> gm["Mit GetMessage abholen"]
gm --> dm["DispatchMessage"]
dm --> wp["In der Fensterprozedur behandeln"]
wp --> gm
Abbildung 1: Das Herz einer GUI-App ist die Nachrichtenschleife; jeder Ereignishandler läuft als eine Iteration dieser Schleife.
Diese Struktur hat eine wichtige Folge. Wenn Sie zeitraubende Arbeit innerhalb der Fensterprozedur (eines Ereignishandlers) tun, kann die Schleife in der Zwischenzeit die nächste Nachricht nicht abholen. Sie kann weder auf Klicks noch auf Neuzeichenanforderungen reagieren — das ist es, was ein „Hänger“ wirklich ist.
Es lohnt sich auch aufzunehmen, dass Nachrichten auf zwei Pfaden zugestellt werden. PostMessage legt die Nachricht auf die Warteschlange und kehrt sofort zurück, und die Schleife holt und verarbeitet Nachrichten der Reihe nach. SendMessage dagegen ruft die Fensterprozedur direkt auf und kehrt nicht zum Aufrufer zurück, bis die Verarbeitung fertig ist.36 Dieser Unterschied trägt geradewegs in die Deadlock-Erörterung in Kapitel 4.
flowchart TB
accTitle: Zwei Pfade der Nachrichtenzustellung
accDescr: PostMessage legt die Nachricht auf die Warteschlange und kehrt sofort zurück; die Nachrichtenschleife holt und verarbeitet Nachrichten der Reihe nach. SendMessage ruft die Prozedur direkt auf und kehrt nicht zum Aufrufer zurück, bis die Verarbeitung fertig ist
pm["PostMessage"] --> q2["Auf die Warteschlange legen(kehrt sofort zurück)"]
q2 --> loop["Schleife holt und verarbeitet der Reihe nach"]
sm["SendMessage"] --> direct["Die Prozedur direkt aufrufen"]
direct --> w2["Kehrt nicht zurück, bis die Verarbeitung fertig ist"]
Abbildung 2: Obwohl beide „eine Nachricht senden“, haben ein eingereihtes Post und ein Send, das auf Fertigstellung wartet, völlig verschiedene Naturen.
3. Wie „Keine Rückmeldung“ beurteilt wird — Die 5-Sekunden-Regel und das Geisterfenster
Wie weiß das Betriebssystem also, dass „diese App hängt“? Das Kriterium ist offiziell dokumentiert. Das Betriebssystem behandelt ein Fenster als ohne Rückmeldung, wenn es nicht auf Eingabe wartet, nicht in seiner Startsequenz ist und 5 Sekunden lang PeekMessage (Nachrichtenabholung) nicht aufgerufen hat.1 Mit anderen Worten: Das Betriebssystem beobachtet, ob „die Nachrichtenschleife tatsächlich dreht“, so wie Sie einen Puls nehmen würden, und wenn 5 Sekunden lang kein Puls da ist, urteilt es, das Fenster gebe keine Rückmeldung (die Dokumentation stellt fest, dass dieser 5-Sekunden-Wert sich in Zukunft ändern kann). Die Urteilseinheit ist das Fenster und der GUI-Thread, dem es gehört; in einer App mit mehreren UI-Threads bedeutet das Hängen eines Threads nicht, dass Fenster auf einem anderen Thread tot sind. Der Thread, den Sie in einem Dump ansehen, ist der Besitzer des hängenden Fensters.
Was mit einem Fenster der obersten Ebene geschieht, das beurteilt wurde, ist ebenfalls dokumentiert. Das Betriebssystem verbirgt das ursprüngliche Fenster und ersetzt es durch ein „Geisterfenster“, das dieselbe Z-Reihenfolge, Position, Größe und Erscheinung hat. Alles, was der Benutzer damit tun kann, ist Verschieben, Größenändern oder (zwangsweise) Schließen. Die App darin antwortet tatsächlich nicht, also funktionieren keine anderen Operationen.2
flowchart TB
accTitle: Urteil des hängenden Fensters und der Tausch gegen ein Geisterfenster
accDescr: Wenn der UI-Thread auf schwerer Arbeit blockiert ist und die Nachrichtenabholung 5 Sekunden stoppt, urteilt das Betriebssystem, das Fenster gebe keine Rückmeldung, verbirgt das Original, wechselt ein Geisterfenster derselben Erscheinung ein und bietet dem Benutzer nur Verschieben, Minimieren und Schließen
busy["UI-Thread auf schwerer Arbeit blockiert"] --> stop["Nachrichtenabholung stoppt"]
stop --> judge{"5 Sekunden verstrichen?"}
judge -->|"nein"| stop
judge -->|"ja"| ghost["Geisterfenster einwechseln"]
ghost --> u1["Titel zeigt(Keine Rückmeldung)"]
ghost --> u2["Milchig weiß; nur Verschieben und Schließen"]
Abbildung 3: Sowohl der Text „Keine Rückmeldung“ als auch der weiße Bildschirm gehören zum Geisterfenster, das das Betriebssystem eingewechselt hat, nicht zur hängenden App.
Die Zeichenfolge „(Keine Rückmeldung)“, die in der Titelleiste erscheint, und das milchig-weiße Aussehen unter dem Aero-Thema gehören beide zu diesem Geisterfenster. Zwei praktische Folgen ergeben sich.
- Zu dem Zeitpunkt, zu dem „Keine Rückmeldung“ angezeigt wird, hat der Thread, dem dieses Fenster gehört, mindestens 5 Sekunden lang keine Nachrichten verarbeitet. Es ist nicht so, dass „die Anzeige zu früh kam“ — der UI-Thread ist eindeutig blockiert.
- Ein Geisterfenster wird nicht angelegt, während ein Debugger verbunden ist.2 Wenn es so aussieht, als ginge es „unter dem Debugger nie auf Keine Rückmeldung, aber im Release schon“, kann der Hänger selbst derselbe sein und nur die Anzeige verschieden.
Es gibt auch eine API, DisableProcessWindowsGhosting, die diesen Tausch für den ganzen Prozess deaktiviert.7 Sie ist für Sonderfälle wie Kiosk-Terminals gedacht, bei denen Sie nicht wollen, dass das Betriebssystem ein Fenster von sich aus bedienbar aussehen lässt. Sie aufzurufen stoppt das Erscheinen der Anzeige „Keine Rückmeldung“, aber die Tatsache, dass die App hängt, ändert sich nicht. Verstehen Sie, dass dies nichts ist, was eine allgemeine App als Gegenmaßnahme gegen „Keine Rückmeldung“ nutzt.
4. Warum Apps hängen — Klassische Muster, die den UI-Thread blockieren
Die Ursache, heruntergekocht, ist ein einzelner Punkt — „der UI-Thread kommt nicht zur Nachrichtenschleife zurück“ — aber die Formen, die Sie in der Praxis treffen, fallen in ein paar Klassiker.
flowchart TB
accTitle: Einteilung klassischer Ursachen, die den UI-Thread blockieren
accDescr: Die vier klassischen Familien — synchrones I/O und Netzaufrufe, Sperrwarten, SendMessage über Threads hinweg und COM-STA-Beteiligung — laufen alle auf denselben einzelnen Punkt hinaus, dass der UI-Thread nicht zur Nachrichtenschleife zurückkehren kann
kind{"Welche klassische Ursache?"}
kind --> io{"I/O oder eine Sperre?"}
kind --> other{"SendMessage oder COM?"}
io --> c1["Synchrones I/O und Netz"]
io --> c2["Sperrwarten"]
other --> c3["SendMessage"]
c3 -.-> c3n["über Threads hinweg"]
other --> c4["COM-STA-Beteiligung"]
c1 --> core["UI kann nicht zurückkehren"]
c2 --> core
c3 --> core
c4 --> core
core --> ar["Prüfung auf Keine Rückmeldung"]
Abbildung 4: Das sichtbare Symptom ist dasselbe, aber der Schuldige, der den Thread blockiert, fällt in vier Familien, und die Gegenmaßnahme unterscheidet sich für jede.
Synchrones I/O und Netzaufrufe. Das ist das Häufigste. Das Muster, synchron innerhalb eines Schaltflächenklick-Handlers ein großes Dateilesen oder -schreiben, eine Datenbankabfrage, einen Web-API-Aufruf oder den Zugriff auf eine Datei auf einem Netzlaufwerk zu tun. Auf einer Entwicklungsmaschine ist es in einem Bruchteil einer Sekunde fertig, also merken Sie es nie; Produktionsnetzlatenz oder ein Hänger des Dateiservers macht daraus ein Warten von mehreren zehn Sekunden, und Sie bekommen Tickets, dass „es gelegentlich hängt“. Netzlaufwerke haben lange Timeouts, wenn die Verbindung tot ist, und sie machen das Symptom dramatisch schlimmer.
Sperrwarten. Das Muster, in dem der UI-Thread versucht, eine Sperre auf Daten zu nehmen, die mit einem Worker-Thread geteilt werden, und am Ende auf einen Worker wartet, der diese Sperre lange hält. Sperrdisziplin ist ausführlich in der praktischen Multithreading-Serie behandelt.
SendMessage über Threads hinweg. SendMessage kehrt nicht zurück, bis die Prozedur des Zielfensters die Verarbeitung beendet hat.6 Wenn Sie es an ein Fenster auf einem anderen Thread senden, wird der Sender warten gelassen, bis dieser Thread in einem Zustand ist, in dem er Nachrichten verarbeiten kann. Wenn der Zielthread selbst auf etwas wartet, haben Sie einen Nachrichtendeadlock, in dem jede Seite auf die andere wartet.3 Das Senden an HWND_BROADCAST insbesondere zieht Sie hinein, sobald ein einziges Fenster keine Rückmeldung gibt. Wenn Sie sich das Warten nicht leisten können, erwägen Sie SendMessageTimeout oder PostMessage, das nicht auf eine Antwort wartet.8
sequenceDiagram
accTitle: Deadlock durch SendMessage über Threads hinweg
accDescr: Wenn ein Worker-Thread SendMessage an ein Fenster des UI-Threads sendet, während der UI-Thread blockiert auf das Ergebnis des Workers wartet, wartet jede Seite darauf, dass die andere fertig wird, und Sie haben einen Deadlock
participant U as UI-Thread
participant W as Worker-Thread
U->>U: Wartet auf das Ende des Workers(blockiert)
W->>U: SendMessage(kehrt nicht zurück, bis verarbeitet)
Note over U: Kann Nachrichten nicht verarbeiten(blockiert)
Note over W: Kann von SendMessage nicht zurückkehren
Note over U,W: Warten aufeinander — Deadlock
Abbildung 5: „Der UI-Thread wartet auf den Worker, und der Worker wartet über SendMessage auf den UI-Thread“ ist ein klassischer Deadlock.
COM-Apartment-Beteiligung. Aufrufe an ein STA-Objekt werden als Fensternachrichten zugestellt, also werden, wenn der UI-Thread (STA) blockiert ist, COM-Aufrufe von anderen Threads als Kollateralschaden ebenfalls blockiert. Diese Struktur ist im Artikel zu COM STA/MTA erklärt.
Die Anhäufung von „es ist nur ein Moment“. Selbst ein 50-ms-synchroner Aufruf, 100-mal in einer Schleife aufgerufen, sind 5 Sekunden. Die Schwelle für Keine Rückmeldung ist 5 Sekunden, aber wahrgenommene „Trägheit“ beginnt um 100 ms. Eine Daumenregel für den Entwurf ist „der UI-Thread darf nur für Millisekunden blockiert sein“.
5. Entwürfe, die nicht hängen — Schwere Arbeit vom UI-Thread herunternehmen
Das Entwurfsprinzip ist eine Sache: Nehmen Sie zeitraubende Arbeit vom UI-Thread herunter. In C# (WinForms/WPF) ist async/await der kürzeste richtige Ansatz.
private async void RunButton_Click(object sender, EventArgs e)
{
runButton.Enabled = false;
try
{
// CPU-heavy work, or APIs that are only synchronous, go to a worker via Task.Run
var result = await Task.Run(() => HeavyCalculation(input));
// For I/O, use APIs that are natively async (they do not consume a thread either)
var data = await httpClient.GetStringAsync(url);
// After await you are back on the UI thread, so you can touch controls directly
resultLabel.Text = result + data.Length.ToString();
}
catch (Exception ex)
{
// An exception leaking from an async void handler will take the app down. Catch it here
MessageBox.Show($"The operation failed: {ex.Message}");
}
finally
{
runButton.Enabled = true;
}
}
Es gibt drei Punkte. Erstens: Während await wartet, ist der UI-Thread zurück in der Nachrichtenschleife, also gehen Sie nicht auf Keine Rückmeldung. Zweitens: Die Fortsetzung nach await kehrt zum UI-Thread zurück, sodass Sie Steuerelemente danach normal anfassen können (ein Steuerelement direkt von einem Worker-Thread anzufassen ist verboten; wenn Sie müssen, nutzen Sie Control.Invoke / Dispatcher.InvokeAsync).4 Drittens: Deaktivieren Sie die Schaltfläche, während die Arbeit läuft, und töten Sie Wiedereintritt sonst durch Entwurf.
Das Bild ist in nativem Win32 dasselbe: Übergeben Sie die Arbeit an einen Worker-Thread, benachrichtigen Sie den UI-Thread über die Fertigstellung als eigene Nachricht über PostMessage, und aktualisieren Sie die UI in der Fensterprozedur. PostMessage legt die Nachricht nur auf die Warteschlange und kehrt sofort zurück, also ist die Worker-Seite ebenfalls nicht blockiert.6 Schreiben Sie das Warten auf den Worker-Thread selbst mit der Disziplin, die im Artikel zu Bedingungsvariablen behandelt ist.
flowchart TB
accTitle: Rollenteilung in einer App, die nicht hängt
accDescr: Der UI-Thread ist nur für das Annehmen von Eingabe, das Anzeigen von Fortschritt und das Annehmen von Abbruch verantwortlich; ein Worker-Thread führt die schwere Arbeit aus und gibt die Fertigstellung über PostMessage oder eine await-Fortsetzung an den UI-Thread zurück
ui["UI-Thread: Eingabe, Fortschritt, Abbruch"] -->|"Die Arbeit übergeben"| w["Worker-Thread: schwere Arbeit"]
w -->|"PostMessage / await-Fortsetzung"| ui
ui -.-> ng["Kein synchrones I/O oder langes Rechnen auf dem UI-Thread"]
Abbildung 6: Halten Sie den UI-Thread als „Empfangstheke“, übergeben Sie schwere Arbeit immer an einen Worker und nehmen Sie nur die Fertigstellungsbenachrichtigung.
Was Sie vermeiden wollen, ist die Technik, Application.DoEvents() oder eine PeekMessage-Schleife zwischen Brocken schwerer Arbeit einzufügen, nur um die Anzeige am Leben zu halten. Sie umgehen Keine Rückmeldung, aber beliebige Ereignishandler treten mitten in der Arbeit wieder ein. Ein zweiter Klick auf die Schaltfläche, das Schließen des Formulars während der Verarbeitung, das Feuern eines Timers — jedes davon kann Daten verderben, die noch verarbeitet werden, und die Fehler sind zeitabhängig und schwer zu reproduzieren. Halten Sie manuelles Pumpen der Nachrichtenschleife innerhalb einer begrenzten Struktur wie einem modalen Fortschrittsdialog, und lösen Sie es in der Regel durch Trennung.
sequenceDiagram
accTitle: Zeitachse eines Wiedereintrittsfehlers durch DoEvents
accDescr: DoEvents mitten in schwerer Arbeit aufzurufen lässt den Ereignishandler eines eingereihten Klicks unterbrechen und laufen, Daten umschreiben, die noch verarbeitet werden, und dann die ursprüngliche Arbeit fortsetzen, was zeitabhängige Datenverderbnis erzeugt
participant U as UI-Thread
U->>U: Schwere Arbeit beginnt(Daten werden verarbeitet)
U->>U: DoEvents(eingereihte Nachrichten verarbeiten)
Note over U: Der Handler des erneuten Schaltflächenklicks unterbricht
U->>U: Die unterbrechende Arbeit schreibt die Daten um
U->>U: Ursprüngliche Arbeit setzt fort(Daten bereits inkonsistent)
Abbildung 7: DoEvents löscht „Keine Rückmeldung“ im Tausch dafür, beliebige Ereignisse mitten in die Arbeit einzuladen.
Für lang laufende Arbeit nehmen Sie Fortschrittsanzeige und Abbruch ebenfalls in den Entwurf auf. Senden Sie Fortschritt mit IProgress<T> an die UI und teilen Sie Unterbrechung mit einem CancellationToken mit, und der Benutzer kann sehen, dass „es arbeitet“, und wird nicht nach einem Zwangsabbruch greifen (der oft eine Ursache von Datenverderbnis ist).
flowchart TB
accTitle: Fortschritts- und Abbruchablauf für lang laufende Arbeit
accDescr: Der Worker-Thread sendet Fortschritt über IProgress an den UI-Thread; eine Abbruchaktion auf der UI erreicht den Worker über einen CancellationToken; der Worker stoppt an einer günstigen Grenze und räumt auf
w3["Worker: lang laufende Arbeit"] -->|"Fortschritt über IProgress"| ui2["UI: Fortschritt und eine Stopp-Schaltfläche"]
ui2 -->|"CancellationToken"| w3
w3 --> stop2["An einer Grenze stoppen und aufräumen"]
Abbildung 8: Fortschritt ist „Worker → UI“; Abbruch ist „UI → Worker“. Nehmen Sie diesen dünnen Zweiwegekanal von Anfang an in den Entwurf auf.
6. Den hängenden Moment untersuchen
Bei einer Untersuchung von „es hängt gelegentlich“ ist das Wertvollste der Thread-Zustand im genauen Moment, in dem es hängt. Starten Sie neu, und der Beweis ist weg.
Nehmen Sie einen Dump. Auf der Registerkarte Details des Task-Managers Rechtsklick auf den Zielprozess → „Abbilddatei erstellen“. Das allein gibt Ihnen einen vollständigen Dump mit dem Stack jedes Threads. Dem IT-Personal, das die Tickets entgegennimmt, nur zu sagen „wenn es hängt, nehmen Sie das, bevor Sie es schließen“ ändert die Erfolgsrate der Untersuchung stark. Zum Aufbau eines Sammelmechanismus siehe den Artikel zum Sammeln von Absturz-Dumps.
Schauen Sie auf den Stack des UI-Threads. Öffnen Sie den Dump in WinDbg und schauen Sie auf den Stack des Threads, der die Nachrichtenschleife pumpt (gewöhnlich Thread 0). Synchrones I/O erscheint als ReadFile oder eine Netz-API, ein Sperrwarten als ein Aufruf der Familie WaitFor…, und SendMessage über Threads hinweg als Warten innerhalb von SendMessage — so, wie es ist. Wie man es liest, ist im einführenden WinDbg-Artikel erklärt.
Schauen Sie es lebend an. Mit Process Explorer können Sie die Thread-Liste und die Stacks an Ort und Stelle prüfen. Wenn es stetig träge ist, nehmen Sie eine WPR-Aufzeichnung und analysieren Sie die Warten des UI-Threads über die Zeit (WPR/WPA in der Praxis).
flowchart TB
accTitle: Grundverfahren zur Untersuchung von Keine Rückmeldung
accDescr: Nehmen Sie einen Dump im hängenden Moment, schauen Sie auf den Stack des UI-Threads, identifizieren Sie, ob er auf synchronem I/O, einem Sperrwarten oder SendMessage über Threads hinweg steht, und verbinden Sie das mit der passenden Entwurfsreparatur
hang["Der hängende Moment"] --> dump["Einen Dump nehmen(vor dem Schließen)"]
dump --> stack["Den Stack des UI-Threads ansehen"]
stack --> io["Synchrones I/O oder Netzwarten"]
stack --> lock["Sperrwarten"]
stack --> sm["SendMessage über Threads hinweg"]
io -.-> fix["Die Stelle auf einen Worker trennen"]
lock -.-> fix
sm -.-> fix
Abbildung 9: Der Star der Untersuchung ist „ein Dump des hängenden Moments“; der Stack des UI-Threads ist selbst die Einteilung der Ursache.
Sie können auch standardisieren, wie Sie einen ersten Schnitt vom Symptom nehmen. Wenn es bei einer bestimmten Operation immer hängt, verdächtigen Sie zuerst synchrones I/O innerhalb dieses Handlers. Wenn es selten und ohne Zusammenhang mit einer Operation hängt, verdächtigen Sie Sperrreihenfolge oder einen SendMessage-Deadlock über Threads hinweg, und gleichen Sie in dem Dump die Warteziele beider Threads ab. Wenn es nur in einer bestimmten Umgebung hängt, verdächtigen Sie Timeouts durch Umgebungsfaktoren wie ein Netzlaufwerk, einen Proxy oder Antivirensoftware.
7. Zusammenfassung
- „Keine Rückmeldung“ ist ein Mechanismus, in dem das Betriebssystem urteilt, dass eine App 5 Sekunden lang keine Nachricht abgeholt hat, und ein Geisterfenster einwechselt. Das, was die Anzeige aufstellt, ist das Betriebssystem, nicht die App.
- Die Ursache eines Hängers ist ein einzelner Punkt: „der UI-Thread kann nicht zur Nachrichtenschleife zurückkehren“. Synchrones I/O, das Netz, Sperrwarten und SendMessage über Threads hinweg sind die Klassiker.
- Die Gegenmaßnahme ist, schwere Arbeit vom UI-Thread herunterzunehmen. In C#
async/await+Task.Run; in Win32 ein Worker-Thread +PostMessage. Verhindern Sie Wiedereintritt während der Ausführung durch Entwurf, etwa durch Deaktivieren der Schaltfläche. - Das Ausweichen mit
DoEventsist im Tausch gegen Wiedereintrittsfehler.DisableProcessWindowsGhostingentfernt nur die Anzeige. Keines von beiden ist eine Ursachenlösung. - Für die Untersuchung ist ein Dump des „hängenden Moments“ das Wichtigste. Die Ursache steht fast immer so, wie sie ist, auf dem Stack des UI-Threads.
Aus Benutzersicht ist „Keine Rückmeldung“ „es ist kaputt“, aber sobald Sie den Mechanismus kennen, können Sie es in den genauen Satz übersetzen „der UI-Thread ist 5 Sekunden nicht zurückgekommen“. Von diesem einen Satz rückwärts arbeitend fallen die Kandidatenursachen, die Reparatur und das Untersuchungsverfahren alle natürlich heraus.
Weiterführende Artikel
- Scheinwecken — Warum Bedingungsvariablen „ohne Benachrichtigung“ aufwachen und wie Sie unter Windows richtig warten
- Praktische Best Practices für Multithreading: .NET-Edition — Was Sie entscheiden sollten, bevor Sie weitere Threads hinzufügen
- Grundlagenwissen zu COM STA/MTA – Threadmodelle und wie man Hänger vermeidet
- Absturz-Dumps mit WinDbg + SOS lesen — Ein praxistauglicher Leitfaden zur Analyse nach der Sammlung
- Process Explorer / Handle / VMMap in der Praxis — Hängern, Lecks und „Datei wird verwendet“ vom aktuellen Zustand aus nachjagen
- Windows-Herunterfahren aus Sicht Ihrer App — Beendigungsbenachrichtigungen, Neustarts und Stromverlust richtig überstehen
Zugehörige Beratungsfelder
KomuraSoft LLC übernimmt Ursachenuntersuchungen von Geschäftsanwendungen, die „gelegentlich hängen“ oder auf „Keine Rückmeldung“ gehen (Dump-Analyse und Trace-Analyse), das Refactoring von Legacy-UI-Code voller synchroner Arbeit in async/await und Worker-Thread-Trennung sowie Reviews von UI-Entwürfen, die nicht einfrieren. Auch wenn Sie noch kein Reproduktionsverfahren haben, können wir ab dem Entwurf helfen, wie Beweis gesammelt wird.
- Fehleruntersuchung und Ursachenanalyse
- Technische Beratung und Design-Review
- Windows-Anwendungsentwicklung
- Kontakt
Quellen
-
Microsoft Learn, IsHungAppWindow function (winuser.h). Zum Urteilskriterium, dass eine App als ohne Rückmeldung behandelt wird, wenn sie „nicht auf Eingabe wartet, nicht in ihrer Startsequenz ist und PeekMessage für das interne Timeout von 5 Sekunden nicht aufgerufen hat“; dazu, dass dieses 5-Sekunden-Kriterium sich ändern kann; sowie dazu, dass die Funktion für ein Geisterfenster immer TRUE zurückgibt. ↩ ↩2
-
Microsoft Learn, GetMessage function (winuser.h). Dazu, dass das System ein Fenster der obersten Ebene als ohne Rückmeldung behandelt, wenn es mehrere Sekunden auf Nachrichten nicht antwortet, und es durch ein Geisterfenster derselben Z-Reihenfolge, Position, Größe und Erscheinung ersetzt; dazu, dass der Benutzer es nur verschieben, in der Größe ändern oder schließen kann; sowie dazu, dass ein Geisterfenster nicht angelegt wird, während ein Debugger verbunden ist. ↩ ↩2 ↩3
-
Microsoft Learn, About Messages and Message Queues. Dazu, dass Windows-Apps ereignisgetrieben sind und die Fensterprozedur Nachrichten verarbeitet; zur Unterscheidung zwischen eingereihten Nachrichten und direkt gesendeten Nachrichten; zum Tausch eines Fensters ohne Rückmeldung gegen ein Geisterfenster; sowie zum Abschnitt über Deadlock durch Threads, die einander Nachrichten senden. ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, How to make thread-safe calls to controls (Windows Forms). Dazu, dass WinForms-Steuerelemente nicht sicher von einem anderen Thread als dem, der sie angelegt hat, angefasst werden dürfen; zur Nutzung von Invoke/BeginInvoke für Aktualisierungen von einem anderen Thread; sowie zu sicheren asynchronen Mustern mit async/await oder BackgroundWorker. ↩ ↩2
-
Microsoft Learn, Using Messages and Message Queues. Zu einer typischen Umsetzung einer Nachrichtenschleife mit GetMessage, TranslateMessage und DispatchMessage sowie dazu, wie man eine Nachrichtenwarteschlange prüft. ↩
-
Microsoft Learn, SendMessage function (winuser.h). Dazu, dass SendMessage die Fensterprozedur des angegebenen Fensters aufruft und nicht zurückkehrt, bis die Verarbeitung fertig ist; dazu, dass ein Senden an ein Fenster auf einem anderen Thread den Sender warten lässt, bis dieser Thread die Nachricht verarbeitet; sowie zum Unterschied zu PostMessage, das die Nachricht auf die Warteschlange legt, ohne auf eine Antwort zu warten. ↩ ↩2 ↩3
-
Microsoft Learn, DisableProcessWindowsGhosting function (winuser.h). Dazu, dass Sie für den aufrufenden GUI-Prozess die Geisterfensterfunktion deaktivieren können, die ein Fenster ohne Rückmeldung minimierbar, verschiebbar und schließbar macht; sowie dazu, dass die Deaktivierung für die Lebensdauer des Prozesses gilt. ↩
-
Microsoft Learn, SendMessageTimeout function (winuser.h). Dazu, eine Nachricht mit einem Timeout senden zu können; sowie zu einem Flag (SMTO_ABORTIFHUNG), das ohne Warten zurückkehrt, wenn das Fenster keine Rückmeldung gibt (als hängend beurteilt wurde). ↩
Verwandte Artikel
Aktuelle Artikel mit denselben Schlagwörtern führen zu verwandten Themen weiter.
DllMain und die Ladersperre — Der wahre Grund, warum man Ihnen sagt, „in der DLL-Initialisierung nichts zu tun“
Warum Sie aus DllMain weder LoadLibrary aufrufen noch mit anderen Threads synchronisieren dürfen. Anhand von Primärquellen erklärt dieser...
Die Win32-Thread-Pool-API — Nebenläufigkeit ohne eigene Threads, über CreateThreadpoolWork
Verstreuen Sie CreateThread-Aufrufe über Ihren nativen Code? Dieser Artikel erklärt die in Vista neu entworfene Win32-Thread-Pool-API — d...
Apps, die nach dem Fortsetzen aus dem Schlaf kaputtgehen — Wie Windows-Energieereignisse funktionieren und wie Sie Geschäftsanwendungen bauen, die sie überstehen
Sie haben den Laptop aufgeklappt, und die Verbindungen der Geschäftsanwendung waren tot — die Ursache ist ein Entwurf, der Schlaf nie ein...
Scheinwecken — Warum Bedingungsvariablen „ohne Benachrichtigung“ aufwachen und wie Sie unter Windows richtig warten
Das Warten einer Bedingungsvariable kann zurückkehren, auch wenn keine Benachrichtigung angekommen ist (ein Scheinwecken). Dieser Artikel...
Wie Zwischenablage und Drag-and-Drop funktionieren — OLE-Datenübertragung in Geschäftsanwendungen richtig behandeln
Eine Excel-Tabelle einfügen und die Formatierung fällt auseinander; die Quell-App schließen und Sie können nicht mehr einfügen — beides k...
Verwandte Themen
Diese Seiten ordnen den Artikel in einen größeren Leistungs- und Entscheidungskontext ein.
Technische Windows-Themen
Portal zu Windows-Entwicklung, Fehleranalyse und der Nutzung bestehender Assets.
UI-Threading und Timer
WPF-/WinForms-UI-Thread, asynchrone Abläufe, Dispatcher und Timer-Entscheidungen.
Fehleranalyse und Langzeitprobleme
Sporadische Fehler, Kommunikationsdiagnose, Langzeitabstürze und Tests von Fehlerpfaden.
Leistungen zu diesem Thema
Dieser Artikel ist direkt mit den folgenden Leistungen verbunden.
Windows-App-Entwicklung
Geschäftsanwendungen, Geräteintegration und Kommunikationstools von den Anforderungen bis zur Umsetzung.
Häufige Fragen
Fragen, die in Beratungen zu diesem Artikelthema häufig gestellt werden.
- Unter welchen Bedingungen erscheint „Keine Rückmeldung“?
- Das Betriebssystem urteilt, dass ein Fenster hängt, wenn eine App mit Fenster nicht auf Eingabe wartet, nicht in ihrer Startsequenz ist und 5 Sekunden lang keine Nachricht abgeholt hat (PeekMessage). Das hängende Fenster der obersten Ebene wird verborgen und durch ein „Geisterfenster“ derselben Position, Größe und Erscheinung ersetzt. Der Titeltext „(Keine Rückmeldung)“ und das milchig-weiße Aussehen gehören zu diesem Geisterfenster, das Ihnen nur Verschieben, Minimieren oder Schließen erlaubt. Mit anderen Worten: „Keine Rückmeldung“ ist nichts, was die App selbst anzeigt — es ist ein Bildschirm, den das Betriebssystem im Namen der App aufstellt.
- Gibt es eine Einstellung, damit „Keine Rückmeldung“ während laufender Arbeit nicht erscheint?
- Der Aufruf von DisableProcessWindowsGhosting deaktiviert den Tausch gegen ein Geisterfenster für diesen Prozess. Das macht den Hänger für den Benutzer nur weniger sichtbar — das Fenster reagiert trotzdem nicht auf Eingabe, und aus Benutzersicht ist es ein vollständiges Einfrieren ohne Weg zum Verschieben oder Schließen. Die echte Lösung ist nicht, die Anzeige zu unterdrücken, sondern schwere Arbeit auf einen Worker-Thread zu verschieben, sodass der UI-Thread nie auch nur eine Zehntelsekunde blockiert ist, geschweige denn fünf. Beachten Sie außerdem, dass das Betriebssystem kein Geisterfenster anlegt, während ein Debugger verbunden ist, sodass es so aussehen kann, als geschähe „Keine Rückmeldung“ während des Debuggens nie.
- Ist es akzeptabel, „Keine Rückmeldung“ mit DoEvents zu vermeiden (die Nachrichtenschleife manuell zu pumpen)?
- Es wird nicht empfohlen. Das Drehen von DoEvents oder einer PeekMessage-Schleife mitten in schwerer Arbeit umgeht das Urteil des hängenden Fensters, aber jeder Ereignishandler kann dann wieder eintreten — ein zweiter Klick auf die Schaltfläche, das Schließen des Fensters, ein Timer und so weiter. Ein anderer Handler, der Daten umschreibt, die noch verarbeitet werden, oder ein Formular anfasst, das geschlossen sein sollte, und eine Ausnahme wirft, erzeugt Wiedereintrittsfehler, die zeitabhängig und schwer zu reproduzieren sind — schlimmer als „Keine Rückmeldung“ selbst. Der richtige Ansatz ist, die Arbeit selbst mit Task.Run oder Ähnlichem auf einen Worker-Thread zu verschieben und den UI-Thread nur für die Fortschrittsanzeige und das Annehmen von Abbruch verantwortlich zu lassen.
- Wie aktualisiere ich die UI (Steuerelemente) von einem Worker-Thread?
- WinForms-Steuerelemente und WPF-Elemente dürfen nur von dem Thread angefasst werden, der sie angelegt hat (normalerweise der UI-Thread). Sie direkt von einem Worker-Thread anzufassen verursacht Ausnahmen oder undefiniertes Verhalten. In C# ist async/await der einfachste Weg: Die Fortsetzung nach await kehrt zum aufrufenden UI-Thread zurück, sodass Sie Steuerelemente nach dem await normal aktualisieren können. Um ausdrücklich zu wechseln, nutzen Sie Control.Invoke/BeginInvoke in WinForms und Dispatcher.InvokeAsync in WPF. In nativem Win32 ist das etablierte Muster, dass der Worker-Thread dem UI-Thread mit PostMessage eine eigene Fertigstellungsnachricht sendet und die Fensterprozedur die UI aktualisiert.
- Wie untersuche ich, warum eine App „Keine Rückmeldung“ zeigt?
- Das Wichtige ist, den Zustand im hängenden „Moment selbst“ festzuhalten. Nehmen Sie zuerst einen vollständigen Dump über die Registerkarte Details des Task-Managers mit „Abbilddatei erstellen“, und schauen Sie dann in WinDbg auf den Stack des UI-Threads (des Threads, der die Nachrichtenschleife ausführt). Ob er in synchronem I/O, einem Netzwarten, einem Sperrwarten oder dem Warten auf einen anderen Thread über SendMessage steckt, zeigt sich auf dem Stack so, wie es ist. Um einen lebenden Prozess anzusehen, sind die Thread-Liste und die Stack-Ansicht von Process Explorer nützlich; um es über die Zeit zu verfolgen, ist das Erfassen einer WPR-Aufzeichnung wirksam. Siehe auch den einführenden WinDbg-Artikel dieser Site, Process Explorer in der Praxis und WPR/WPA in der Praxis.
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.