«Не отвечает»: как Windows определяет зависание и как проектировать так, чтобы оно не возникало
· Обновлено: · Го Комура · Windows, Разработка под Windows, Устранение неполадок, Многопоточность, WinForms, WPF, Win32 API, Проектирование UI
История изменений (первая версия, опубликована 22 Aug 2026)
- Первая публикация
Цитирование статьи(DOI (зарегистрированный архив): 10.5281/zenodo.22176677)
Приведённые ниже DOI относятся к ранее зарегистрированным архивным версиям, которые могут отличаться от текущего текста. Для ссылки на текущий текст используйте URL этой страницы.
Го Комура (2026). «Не отвечает»: как Windows определяет зависание и как проектировать так, чтобы оно не возникало. KomuraSoft LLC. https://comcomponent.com/ru/blog/windows-app-not-responding-hang-mechanism/
- DOI (зарегистрированный архив)
- 10.5281/zenodo.22176677
- DOI (последняя зарегистрированная версия)
- 10.5281/zenodo.22176678
«Посреди работы экран белеет, а в заголовке пишется „Не отвечает“.» «Пользователи говорят, что иногда зависает, а на машине разработчика это ни разу не воспроизводится.» Для бизнес-приложений Windows это частые жалобы.
Первое, что нужно зафиксировать: надпись «Не отвечает» ставит не само приложение, а Windows. Система замечает, что обработка сообщений окна остановилась, и подменяет исходный экран другим окном. Ключ к причине — что делает UI-поток, которому принадлежит это окно, из-за чего он не может обработать следующее сообщение.12
Статья рассчитана на разработчиков бизнес-приложений под Windows и на сотрудников ИТ, которые принимают заявки о зависаниях. Разбираем в порядке решение ОС → цикл сообщений → разбор по причинам → проектирование без зависания → процедура расследования. Если приложение уже зависло и его нужно разобрать сейчас, начните с главы 7.
1. Сначала вывод: не прятать надпись, а освободить UI-поток
Механизм «Не отвечает», способ исправления и способ расследования становятся понятнее, если разделить их так.
| Что нужно узнать или в чём застряли | Что зафиксировать в первую очередь | Подробности |
|---|---|---|
| На что смотрит Windows, когда считает окно неотвечающим | На окно и на обработку сообщений GUI-потока, которому оно принадлежит. Это не решение по процессу целиком | Глава 2 |
| Почему останавливаются и кнопки, и перерисовка | UI-поток не возвращается из обработчика события или из ожидания и не может извлечь следующее сообщение | Главы 3 и 4 |
| Не хотим, чтобы экран застывал на долгой операции | CPU-работу и API, у которых есть только синхронный вариант, вынести в рабочий поток; для I/O брать асинхронные API | Глава 5 |
| Можно ли обойти надпись через DoEvents или настройку | Это либо приводит к багам повторного входа, либо только прячет надпись. Корневой мерой не считать | Глава 6 |
| Хотим понять, почему иногда зависает | Снять дамп в момент зависания, до выхода или перезагрузки | Глава 7 |
Принцип исправления: на UI-потоке не ждать долго и не считать тяжело. UI-поток отвечает за ввод, отрисовку, показ прогресса и приём отмены и отделён от работы, которая занимает время.3
Кроме того, время до появления «Не отвечает» и время отклика, при котором интерфейс ощущается живым, — разные вещи. Уложиться в 5 секунд недостаточно. Это различие разобрано в 4.5.
2. «Не отвечает» — решение ОС: правило 5 секунд и подменный экран
2.1 Единица решения — окно и владеющий поток, а не процесс целиком
По описанию Microsoft для IsHungAppWindow окно считается неотвечающим, если выполнены следующие условия.1
| Условие | Смысл |
|---|---|
| Не ждёт ввода | Окно не находится в состоянии ожидания ввода |
| Не в обработке запуска | Приложение не выполняет обработку запуска |
| Не извлекает сообщения | 5 секунд внутреннего тайм-аута не вызывало PeekMessage |
ОС смотрит не на то, что считает приложение, а на то, крутится ли цикл сообщений. Цикл сообщений — механизм, который по очереди извлекает запросы ввода и перерисовки и обрабатывает их. Как он работает на практике, объясняется в главе 3.
В тех же материалах прямо сказано, что значение 5 секунд в будущем может измениться. Это внутренний тайм-аут решения «не отвечает», а не проектное правило «UI можно блокировать до 5 секунд».1
Решение принимается не на уровне процесса. В приложении с несколькими UI-потоками одно окно может зависнуть, а окна других потоков продолжают работать. И при расследовании недостаточно смотреть на процесс: нужно найти поток, которому принадлежит зависшее окно.
2.2 Белёсый экран — это «фантомное окно»
Когда окно верхнего уровня признано неотвечающим, Windows скрывает исходное окно и подменяет его фантомным окном с тем же Z-порядком, положением, размером и внешним видом. Надпись «(Не отвечает)» в заголовке и белёсый вид в теме Aero принадлежат этому подменному экрану, а не зависшему приложению.2
| Состояние окна | Что видит пользователь |
|---|---|
| Исходное окно приложения | Не может обрабатывать сообщения; не реагирует на щелчки и перерисовку |
| Фантомное окно, которое ставит ОС | Доступны ограниченные операции: перемещение, изменение размера, свёртывание, закрытие |
То, что подменное окно можно двигать, не значит, что содержимое приложения снова заработало. ОС лишь берёт на себя минимум операций.24
Жалобу «надпись появляется слишком рано» тоже сначала стоит рассматривать как проблему того, что UI-поток долго не возвращается к обработке сообщений. Прежде чем глушить надпись, разбирают остановившуюся работу.
2.3 Если при отладке надписи нет, это ещё не значит, что приложение не зависло
Пока подключён отладчик, ОС не создаёт фантомные окна. Поэтому даже если есть разница вроде «при запуске под отладчиком не появляется, а в обычном запуске пишет „Не отвечает“», UI-поток может быть заблокирован точно так же — отличается только отображение.2
Есть и API, который отключает подмену фантомным окном для процесса, но это не API, который чинит само зависание. Его назначение отдельно разобрано в 6.2.
3. Почему останавливается экран: UI-поток и цикл сообщений
3.1 И ввод, и перерисовку обрабатывает один и тот же поток
GUI-приложения Windows управляются событиями. Они получают сообщения мыши, клавиатуры, запросов перерисовки, таймеров и выполняют соответствующую обработку. У каждого потока, создавшего окно, есть очередь сообщений, и он крутит такой цикл.5
MSG msg;
while (GetMessage(&msg, NULL, 0, 0) > 0) {
TranslateMessage(&msg);
DispatchMessage(&msg); // вызывается оконная процедура
}
GetMessage извлекает сообщение из очереди, а DispatchMessage вызывает оконную процедуру этого окна. Оконная процедура — функция, которая выполняет обработку сообщения. Обработка щелчка по кнопке, перерисовка, обработчики событий WinForms и WPF — всё это в итоге связано с этой обработкой сообщений на UI-потоке.6
flowchart TB
accTitle: Базовая структура цикла сообщений
accDescr: ОС кладёт ввод мыши, клавиатуры и прочее в очередь сообщений потока; цикл UI-потока извлекает его через GetMessage и вызывает оконную процедуру через DispatchMessage, а после обработки возвращается в начало цикла
os["ОС (ввод, запросы перерисовки, таймеры)"] --> q["Очередь сообщений потока"]
q --> gm["Извлечь через GetMessage"]
gm --> dm["DispatchMessage"]
dm --> wp["Обработать в оконной процедуре"]
wp --> gm
Рис. 1: Извлечь сообщение, обработать в оконной процедуре и вернуться в цикл. Это вращение и держит ввод с отрисовкой.
Что будет, если из обработчика щелчка вызвать долгую работу? Пока эта работа не вернёт управление, UI-поток не сможет извлечь следующее сообщение. Щелчки и запросы перерисовки, которые придут позже, уже не обрабатываются. Это базовая структура «застывшего экрана».
3.2 PostMessage и SendMessage возвращаются в разное время
Доставка сообщений идёт двумя путями: положить в очередь и ждать завершения обработки.57
| API | Поведение | Когда возвращается вызывающий |
|---|---|---|
PostMessage |
Кладёт сообщение в очередь; получатель извлекает и обрабатывает | Возвращается, как только сообщение поставлено в очередь. Завершения обработки не ждёт |
SendMessage |
Отправляет сообщение оконной процедуре и заставляет её обработать | Не возвращается, пока оконная процедура не закончит обработку |
Особенно если SendMessage отправлен окну другого потока, отправитель тоже ждёт, пока получатель сможет обработать сообщение. Это различие напрямую ведёт к взаимной блокировке в 4.3.7
4. Причины по категориям: пять шаблонов, которые занимают UI-поток
Надпись «Не отвечает» выглядит одинаково, а то, что занимает поток, разное. Сначала разведите кандидатов, затем подтвердите дампами и трассировками главы 7.
| Кандидат причины | Что происходит на UI-потоке | Как обычно проявляется |
|---|---|---|
| Синхронный I/O или сетевой вызов | Ждёт завершения файла, БД, Web API и т. п. | На машине разработчика быстро, а в продакшене или в конкретной среде зависает |
| Ожидание блокировки | Не может взять блокировку, которую держит рабочий поток, и ждёт | Зависает редко, в зависимости от того, как пересекаются операции |
Межпоточный SendMessage |
Ждёт, пока другой поток обработает сообщение | Попадает во взаимное ожидание или в широковещательную рассылку |
| Вызов в COM STA | Ждёт обработки сообщений целевого STA | Остановка UI-потока распространяется и на COM-вызовы с других потоков |
| Накопление коротких операций | Каждая короткая, но непрерывное выполнение не возвращает в цикл | Операции тяжелеют только когда растёт число элементов |
4.1 Синхронный I/O и сетевые вызовы
Самый частый шаблон — синхронно внутри обработчика щелчка по кнопке читать или писать большой файл, выполнять запрос к базе, вызывать Web API, обращаться к сетевому диску.
То, что на машине разработчика укладывается в доли секунды, при сетевой задержке продакшена или сбое файлового сервера становится ожиданием на десятки секунд. У сетевых дисков при обрыве связи длинный тайм-аут, и это усугубляет симптом. «На машине разработчика было быстро» — не причина ждать на UI-потоке.
4.2 UI-поток ждёт блокировку, которую держит рабочий поток
Блокировки, которые защищают общие данные, тоже останавливают UI. Если рабочий поток долго держит блокировку, а UI-поток тоже пытается её взять, UI-поток ждёт, пока не сможет её получить.
Даже после того как работу вынесли в рабочий поток, экран не освобождается, если UI-поток ждёт завершения этой работы или освобождения блокировки. Дисциплина блокировок подробно разобрана в практической серии по многопоточности.
4.3 Взаимное ожидание завершения через SendMessage
Классическая взаимная блокировка — сочетание, в котором UI-поток ждёт завершения рабочего, а этот рабочий шлёт UI SendMessage и ждёт. UI в ожидании и не может обрабатывать сообщения, рабочий не может вернуться из SendMessage, и оба не двигаются дальше.75
sequenceDiagram
accTitle: Взаимная блокировка из-за межпоточного SendMessage
accDescr: Если рабочий поток отправляет SendMessage окну UI-потока в тот момент, когда UI-поток заблокирован ожиданием результата рабочего, каждый ждёт завершения другого и получается взаимная блокировка
participant U as UI-поток
participant W as Рабочий поток
U->>U: Ждать завершения рабочего (блокировка)
W->>U: SendMessage (не возвращается до обработки)
Note over U: Не может обрабатывать сообщения (ожидание)
Note over W: Не может вернуться из SendMessage
Note over U,W: Взаимное ожидание: взаимная блокировка
Рис. 2: Одного выноса работы в рабочий поток недостаточно. Если UI и рабочий ждут завершения друг друга, получается взаимная блокировка.
Отправка на HWND_BROADCAST тоже требует осторожности. Достаточно одного неотзывчивого окна, чтобы затянуть отправителя. Там, где нельзя ждать бесконечно, рассмотрите SendMessageTimeout с тайм-аутом или PostMessage, который не ждёт завершения обработки.8
4.4 Останавливается COM STA и тянет за собой другие потоки
Вызовы в объект STA с других потоков доставляются как оконные сообщения. Поэтому когда UI-поток (STA) занят, COM-вызовы в него тоже блокируются. Это структура, в которой ждут не только UI, но и вызывающие стороны.
Связь квартир и обработки сообщений разобрана в статье про COM STA/MTA.
4.5 «Один раз — мгновение», повторённое 100 раз
Даже синхронная операция в 50 мс даёт 5 секунд, если её повторить 100 раз. Не судите по тому, что отдельные функции короткие; думайте о суммарном времени, пока UI-поток не вернётся в цикл сообщений.
Ощущение «подтормаживает» начинается примерно со 100 мс. Не целитесь в порог «не отвечает» в 5 секунд; проектируйте так, что UI-поток можно занимать только на миллисекунды.
5. Проектирование без зависания: отделить выполнение работы от обновления экрана
5.1 Разделить вычисления и синхронные API от асинхронного I/O
Принцип исправления — убрать с UI-потока работу, которая занимает время. Но не всё переносится одним и тем же способом.
| Вид работы | Базовый приём в C# | Роль UI-потока |
|---|---|---|
| Вычисления с высокой нагрузкой на CPU | Перенести в рабочий поток через Task.Run |
Асинхронно дождаться завершения и показать результат |
| Обработка, у которой есть только синхронный API | Вынести в рабочий поток | Не занимать UI синхронным ожиданием завершения |
| I/O с асинхронным API | await для GetStringAsync и подобных |
Пока I/O в полёте, вернуться к обработке сообщений |
| Ввод, отрисовка, прогресс, отмена | Вести на UI-потоке | Не смешивать с длинными вычислениями и синхронным I/O |
При асинхронном I/O не нужно занимать поток только ради ожидания. И в примере кода ниже вычисления идут через Task.Run, а HTTP-обмен — через нативный асинхронный API.3
5.2 В C# возвращайтесь на UI через async/await и обновляйте экран там
Следующий пример в обработчике щелчка WinForms отделяет тяжёлую работу от обновления UI. Поскольку пример стартует из обработчика UI-события и сохраняет контекст выполнения UI, продолжение после await возвращается на UI-поток, где можно обновлять элементы управления.3
private async void RunButton_Click(object sender, EventArgs e)
{
runButton.Enabled = false;
try
{
// CPU-нагрузка и API только в синхронном виде — в рабочий поток через Task.Run
var result = await Task.Run(() => HeavyCalculation(input));
// Для I/O — нативно асинхронный API (поток тоже не расходуется)
var data = await httpClient.GetStringAsync(url);
// После await мы снова на UI-потоке, поэтому элементы можно трогать напрямую
resultLabel.Text = result + data.Length.ToString();
}
catch (Exception ex)
{
// Исключение, утекающее из обработчика async void, роняет приложение. Ловите здесь
MessageBox.Show($"Операция не выполнена: {ex.Message}");
}
finally
{
runButton.Enabled = true;
}
}
Из этого стоит взять четыре пункта.
| Место | Замысел |
|---|---|
Ждать завершения через await |
При каждом ожидании возвращать UI-поток в цикл сообщений |
Обновлять экран после await |
Не трогать элементы управления из рабочего потока |
| Отключать кнопку на время выполнения | Не дать повторно запустить ту же операцию |
catch и finally |
Не выпускать исключения из обработчика async void и вернуть состояние кнопки |
Это пример, который показывает разделение ролей; реализация HeavyCalculation и подобного, обработка закрытия формы, прогресс и отмена опущены. Прогресс и прерывание, которые нужны долгой работе, добавляют отдельно, как в 5.4.
Элементы управления WinForms и элементы WPF обрабатывают из потока, который их создал. Прямое обращение из рабочего потока ведёт к исключениям или неопределённому поведению. Чтобы явно вернуться на UI-поток, в WinForms используют Control.Invoke / BeginInvoke, в WPF — Dispatcher.InvokeAsync.3
5.3 В Win32 уведомляйте о завершении через PostMessage
В нативном Win32 разделение ролей то же. Работу отдают рабочему потоку, а по завершении шлют своё сообщение о завершении через PostMessage и обновляют экран в оконной процедуре UI-потока. PostMessage возвращается, как только сообщение поставлено в очередь, поэтому рабочий поток не ждёт, пока UI закончит обработку.7
flowchart TB
accTitle: Разделение ролей в приложении, которое не зависает
accDescr: UI-поток отвечает только за приём ввода, показ прогресса и приём отмены; рабочий поток выполняет тяжёлую работу и возвращает завершение UI-потоку через PostMessage или продолжение await
ui["UI-поток: ввод, прогресс, отмена"] -->|"передать работу"| w["Рабочий поток: тяжёлая работа"]
w -->|"PostMessage / продолжение await"| ui
ui -.-> ng["На UI-потоке запрещены синхронный I/O и долгие вычисления"]
Рис. 3: Тяжёлые вычисления и синхронную работу отдайте рабочему потоку, а обновление экрана верните на UI-поток. Асинхронный I/O, как в 5.1, ждут через асинхронный API.
Само ожидание рабочего потока пишут по дисциплине из статьи про условные переменные. Важно и то, чтобы UI-поток снова не ждал рабочий синхронно.
5.4 Проектируйте прогресс и отмену с самого начала
Даже если экран не застывает, долгое отсутствие изменений не даёт пользователю понять, идёт ли работа. Поэтому показ прогресса и приём отмены проектируйте как часть обработки.
| Направление | Механизм | Роль |
|---|---|---|
| Рабочий → UI | IProgress<T> |
Сообщить экрану, как продвигается работа |
| UI → рабочий | CancellationToken |
Передать запрос на остановку |
Рабочий поток останавливается на удобной границе и делает завершающие действия. Если есть средства прогресса и отмены, пользователи реже доходят до принудительного завершения. Принудительное завершение иногда приводит к порче данных.
6. Два метода, которые выглядят как обход, и их пределы
6.1 DoEvents впускает другие события в середину работы
Если во время тяжёлой работы вставить Application.DoEvents() или цикл PeekMessage, сообщения обрабатываются и надпись «Не отвечает» не появляется. Но пока исходная работа ещё не закончена, начинает выполняться другой обработчик события. Это повторный вход.
Например, посреди преобразования данных вызывают DoEvents, и обрабатывается накопившийся повторный щелчок по кнопке. Этот обработчик переписывает те же данные, после чего исходная работа возобновляется. В таком порядке состояние, на которое рассчитывала исходная работа, уже потеряно.
Повторно входят не только кнопки. Приходят закрытие формы и таймеры. Баги вроде порчи данных в полёте или исключения от обращения к закрытой форме зависят от момента, плохо воспроизводятся и часто расследуются тяжелее, чем само «Не отвечает».
Принцип — не крутить насос вручную, а отделить работу. Ручную обработку сообщений оставляйте внутри ограниченных конструкций вроде модального диалога прогресса. Для обычной долгой работы используйте разделение и защиту от повторного входа из главы 5.
6.2 Отключение фантомных окон не возвращает управление
DisableProcessWindowsGhosting — API, который отключает подмену фантомным окном для вызывающего процесса. Отключение действует, пока жив процесс.4
Он нужен для особых сценариев вроде киоска, где не хотят, чтобы ОС ставила подменное управляемое окно. Факт, что обработка сообщений остановилась, не меняется, и пользователь теряет ещё и средства перемещения и закрытия, которые давало фантомное окно. В обычном приложении это не мера против «Не отвечает».
| Метод | Что меняется | Что остаётся |
|---|---|---|
Ручная прокачка через DoEvents и аналоги |
Посреди работы обрабатываются другие сообщения | Произвольные события входят повторно и могут испортить состояние |
DisableProcessWindowsGhosting |
ОС перестаёт подменять окно | UI-поток остаётся занятым |
| Отделить долгую работу от UI | UI-поток может вернуться к вводу и отрисовке | Нужно ещё спроектировать прогресс, отмену и защиту от повторного входа |
7. Процедура расследования: сохранить момент зависания до того, как закрыть
7.1 Сначала снимите дамп
В расследовании «иногда зависает» самое ценное — состояние потоков в сам момент зависания. Если выйти или перезагрузить, это состояние теряется.
На вкладке «Подробности» Диспетчера задач щёлкните правой кнопкой по целевому процессу и выберите «Создать файл дампа». Получите полный дамп со стеками всех потоков. С теми, кто принимает заявки, тоже согласуйте процедуру: «если зависло — снять дамп до закрытия».
Как строить сбор, см. в статье про сбор дампов сбоев.
7.2 Смотрите поток, которому принадлежит зависшее окно
Откройте дамп в WinDbg и проверьте стек UI-потока. Поток, который крутит цикл сообщений, обычно поток 0, но в некоторых приложениях UI-потоков несколько, поэтому не решайте по номеру: смотрите поток, которому принадлежит зависшее окно.
| Что видно на стеке | Что проверять дальше |
|---|---|
Ожидание в ReadFile или сетевом API |
Доступ к файлу, синхронный I/O, ожидание сетевого ответа |
Ожидание в функции WaitFor… |
Блокировка или объект синхронизации. Завершения чего он ждёт и что делает другая сторона |
Ожидание внутри SendMessage |
Может ли поток-получатель обрабатывать сообщения. Не ждут ли они друг друга |
На стеке UI-потока видно, чего он ждёт. Если подозреваете взаимную блокировку, сопоставьте цели ожидания обоих потоков, а не одного. Как их читать, разобрано во вводной статье по WinDbg.
flowchart TB
accTitle: Базовая процедура расследования «Не отвечает»
accDescr: Снять дамп в момент зависания, посмотреть стек UI-потока, определить, остановился ли он на синхронном I/O, ожидании блокировки или межпоточном SendMessage, и связать это с соответствующей правкой проекта
hang["Момент зависания"] --> dump["Снять дамп (до закрытия)"]
dump --> stack["Посмотреть стек UI-потока"]
stack --> io["Синхронный I/O или сетевое ожидание"]
stack --> lock["Ожидание блокировки"]
stack --> sm["Межпоточный SendMessage"]
io -.-> fix["Вынести место в рабочий поток"]
lock -.-> fix
sm -.-> fix
Рис. 4: По дампу момента зависания проследите, чего ждёт UI-поток. Для I/O сделайте асинхронным или вынесите; для блокировок и SendMessage проверьте ещё и структуру ожидания.
7.3 Выбирайте между живым осмотром и анализом по времени
Кроме дампа есть способ посмотреть живой процесс на месте и способ записать ход времени.
| Что хотите узнать | Способ |
|---|---|
| Сохранить момент зависания и разобрать позже | Снять дамп и разобрать в WinDbg |
| На месте увидеть потоки и стеки живого процесса | Использовать Process Explorer |
| Проследить по времени постоянную вялость или ожидания UI-потока | Снять трассировку WPR и разобрать в WPA |
Как снимать шкалу времени, разобрано в практике WPR/WPA.
7.4 Сузьте кандидатов по симптому, затем подтвердите доказательствами
Условия воспроизведения тоже вход в расследование. Но не решайте причину только по симптому: сверяйте с дампами и трассировками.
| Симптом | Что подозревать сначала | Где проверять |
|---|---|---|
| Всегда зависает на конкретной операции | Синхронный I/O и подобное внутри этого обработчика | Обработка этой операции и стек UI-потока |
| Зависает редко, без связи с операциями | Порядок блокировок или взаимная блокировка межпоточного SendMessage |
Стеки обоих потоков, которые ждут друг друга |
| Зависает только в конкретной среде | Ожидания и тайм-ауты из-за сетевых дисков, прокси, антивируса и т. п. | API, на котором ждут, и время ответа в этой среде |
8. Итог
«Не отвечает» — механизм, в котором Windows определяет, что обработка сообщений окна остановилась, и ставит подменный экран. Единица решения — окно и владеющий GUI-поток, а не процесс целиком. Внутренний тайм-аут в 5 секунд — значение, которое может измениться, и его отличают от комфортного времени отклика UI.12
Чинить нужно не надпись, а вычисление или ожидание, которое надолго занимает UI-поток. CPU-работу и API только в синхронном виде отправляйте в рабочий поток, асинхронный I/O — в асинхронные API, обновление экрана возвращайте на UI-поток. Прогресс, отмену и защиту от повторного входа проектируйте комплектом. Обход через DoEvents или отключение фантомных окон это не заменяет.
Когда причина неизвестна, начните с того, чтобы снять дамп до выхода и посмотреть, чего ждёт поток, которому принадлежит зависшее окно. Замените «выглядит сломанным» на «UI-поток не может вернуться к обработке сообщений» — и места, куда смотреть, связываются с правками проекта.
Похожие статьи
- Ложное пробуждение: почему wait на условной переменной возвращается без уведомления и как ждать в Windows
- Практические рекомендации по многопоточности: .NET — что решить до добавления потоков
- STA и MTA в COM: модель потоков и как не получить зависание
- Как читать аварийные дампы в WinDbg + SOS — практика анализа после сбора
- Process Explorer / Handle / VMMap на практике — зависания, утечки и «файл используется» по состоянию прямо сейчас
- Завершение работы Windows со стороны приложения — уведомления, перезагрузка и отключение питания
Смежные области консультирования
KomuraSoft LLC занимается расследованием причин (анализ дампов и трассировок) бизнес-приложений, которые «иногда зависают» или показывают «Не отвечает», переработкой устаревшего UI-кода, полного синхронной обработки, на async/await и вынос в рабочие потоки, а также ревью UI-проектов, которые не застывают. Даже до того как известна процедура воспроизведения, можно начать с проектирования сбора доказательств.
- Расследование сбоев и анализ причин
- Техническая консультация и ревью проекта
- Разработка Windows-приложений
- Связаться с нами
Справочные ссылки
-
Microsoft Learn, IsHungAppWindow function (winuser.h). О критерии, по которому приложение считается неотвечающим, если «не ждёт ввода, не выполняет обработку запуска и в течение внутреннего тайм-аута в 5 секунд не вызывало PeekMessage»; о том, что этот критерий в 5 секунд может измениться; и о том, что для фантомного окна функция всегда возвращает TRUE. ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, GetMessage function (winuser.h). О том, что система считает окно верхнего уровня неотвечающим, если оно несколько секунд не отвечает на сообщения, и подменяет его фантомным окном с тем же Z-порядком, положением, размером и внешним видом; о том, что пользователь может только перемещать, изменять размер или закрывать его; и о том, что при подключённом отладчике фантомное окно не создаётся. ↩ ↩2 ↩3 ↩4 ↩5
-
Microsoft Learn, How to make thread-safe calls to controls (Windows Forms). О том, что к элементам управления WinForms небезопасно обращаться из любого потока, кроме создавшего их; о том, что для обновления с другого потока используют Invoke/BeginInvoke; и о безопасных асинхронных шаблонах через async/await или BackgroundWorker. ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, DisableProcessWindowsGhosting function (winuser.h). О том, что для вызывающего GUI-процесса можно отключить функцию фантомного окна, которая делает неотвечающее окно сворачиваемым, перемещаемым и закрываемым; и о том, что отключение действует, пока жив процесс. ↩ ↩2
-
Microsoft Learn, About Messages and Message Queues. О том, что приложения Windows управляются событиями и оконная процедура обрабатывает сообщения; о различии между сообщениями через очередь и сообщениями, отправленными напрямую; о подмене неотвечающего окна фантомным; и о разделе про взаимную блокировку из-за того, что потоки шлют сообщения друг другу. ↩ ↩2 ↩3
-
Microsoft Learn, Using Messages and Message Queues. О стандартной реализации цикла сообщений через GetMessage, TranslateMessage и DispatchMessage и о том, как исследовать очередь сообщений. ↩
-
Microsoft Learn, SendMessage function (winuser.h). О том, что SendMessage вызывает оконную процедуру указанного окна и не возвращается, пока обработка не завершится; о том, что отправка окну другого потока заставляет отправителя ждать, пока тот поток обработает сообщение; и о различии с PostMessage, который кладёт сообщение в очередь, не дожидаясь ответа. ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, SendMessageTimeout function (winuser.h). О том, что сообщение можно отправить с тайм-аутом; и о флаге (SMTO_ABORTIFHUNG), который возвращается без ожидания, если окно не отвечает (признано зависшим). ↩
Похожие статьи
Недавние статьи с теми же тегами помогут подробнее изучить близкие темы.
DllMain и блокировка загрузчика — почему говорят «в инициализации DLL ничего не делайте»
Почему из DllMain нельзя вызывать LoadLibrary и синхронизироваться с потоками. По первоисточникам: как блокировка загрузчика сериализует ...
Пул потоков Win32 — параллелизм через CreateThreadpoolWork без своих потоков
Не плодите ли вы CreateThread по всему нативному коду? Разбираем API пула потоков Win32, переработанный в Vista: четыре объекта work, tim...
Ложное пробуждение: почему wait на условной переменной возвращается без уведомления и как ждать в Windows
wait на условной переменной может вернуться и без уведомления (ложное пробуждение). Почему спецификация это допускает и как правильно жда...
Буфер обмена и перетаскивание: OLE-передача данных в бизнес-приложениях
Таблица из Excel вставляется «криво», а после закрытия исходного приложения вставить уже нельзя — оба случая объясняются тем, что буфер о...
Что продумать перед заказом разработки Windows-приложения
Перед заказом разработки Windows-приложения разберём, что стоит прояснить: доработка существующего ПО, интеграция с оборудованием, COM/Ac...
Связанные темы
Эти страницы показывают тему статьи в более широком контексте услуг и решений.
Технические темы Windows
Раздел о разработке Windows, расследовании сбоев и использовании существующих активов.
Поток UI и таймеры
Поток UI WPF / WinForms, асинхронные операции, Dispatcher и проектирование таймеров.
Услуги по этой теме
Статья напрямую связана со следующими услугами.
Разработка приложений для Windows
Бизнес-приложения, интеграция оборудования и средства связи — от требований до разработки.
Частые вопросы
Вопросы, которые часто возникают при консультациях по теме статьи.
- При каких условиях появляется «Не отвечает»?
- ОС считает окно неотвечающим, когда приложение с окном не ждёт ввода, не выполняет обработку запуска и 5 секунд не извлекало сообщения (PeekMessage). Окно верхнего уровня, которое система признала неотвечающим, скрывается и подменяется «фантомным окном» с тем же положением, размером и внешним видом. Надпись «(Не отвечает)» в заголовке и белёсый вид принадлежат этому фантомному окну: с ним можно только перемещать, изменять размер, сворачивать и закрывать. То есть «Не отвечает» рисует не само приложение: это экран, который ОС ставит вместо него.
- Есть ли настройка, чтобы «Не отвечает» не появлялось, пока идёт работа?
- Вызов DisableProcessWindowsGhosting отключает подмену фантомным окном для этого процесса. Но это только прячет зависание от пользователя: окно по-прежнему не реагирует на действия, и со стороны это выглядит как полный ступор — не переместить и не закрыть. Настоящая мера — не глушить надпись, а перенести тяжёлую работу в рабочий поток, чтобы UI-поток не занимать даже на десятые доли секунды, не говоря уже о пяти. Имейте в виду: пока подключён отладчик, ОС не создаёт фантомное окно, поэтому при отладке может казаться, будто «Не отвечает» никогда не бывает.
- Можно ли обойти «Не отвечает» через DoEvents (ручную прокачку очереди сообщений)?
- Не рекомендуется. Если посреди тяжёлой работы крутить DoEvents или цикл PeekMessage, решение «Не отвечает» обойдёте, но в середину обработки повторно войдут произвольные обработчики: повторный щелчок по кнопке, закрытие окна, таймер. Другой обработчик переписывает данные, которые ещё обрабатываются, или обращается к уже закрытой форме и получает исключение — такие баги повторного входа зависят от момента, плохо воспроизводятся и хуже самого «Не отвечает». Правильный путь: перенести саму работу в рабочий поток через Task.Run или аналог, а UI-потоку оставить только показ прогресса и приём отмены.
- Как обновлять интерфейс (элементы управления) из рабочего потока?
- К элементам управления WinForms и элементам WPF можно обращаться только из потока, который их создал (обычно UI-поток). Прямое обращение из рабочего потока даёт исключения или неопределённое поведение. В C# проще всего async/await: продолжение после await возвращается на вызывающий UI-поток, поэтому после await элементы управления обновляют как обычно. Чтобы переключиться явно, в WinForms используют Control.Invoke/BeginInvoke, в WPF — Dispatcher.InvokeAsync. В нативном Win32 устоявшийся приём: рабочий поток шлёт UI-потоку своё сообщение о завершении через PostMessage, а оконная процедура обновляет экран.
- Как разобрать, почему приложение показывает «Не отвечает»?
- Важно снять состояние в сам «момент» зависания. Сначала на вкладке «Подробности» Диспетчера задач снимите полный дамп через «Создать файл дампа», затем в WinDbg смотрите стек UI-потока (того, который крутит цикл сообщений). На стеке сразу видно, остановился ли он на синхронном вводе-выводе, сетевом ожидании, ожидании блокировки или ожидании другого потока через SendMessage. На живом процессе полезны список потоков и стеки Process Explorer; чтобы идти по времени — снять трассировку WPR. См. также статьи этого сайта: введение в WinDbg, Process Explorer на практике, WPR/WPA на практике.
Об авторе
Страница с профилем автора статьи.
Го Комура
Представитель KomuraSoft LLC
Специализируется на разработке программного обеспечения для Windows, техническом консалтинге и расследовании сбоев, особенно в проектах с унаследованными системами и трудно воспроизводимыми ошибками.