Что на самом деле значит «Не отвечает» — как Windows решает, что приложение зависло, и как проектировать приложения, которые не зависают

· · Windows, Разработка под Windows, Устранение неполадок, Многопоточность, WinForms, WPF, Win32 API, Проектирование UI

«Приложение посреди операции белеет и показывает (Не отвечает).» «Приходят заявки, что иногда зависает, но на машине разработки никогда не воспроизводится.» — Для бизнес-приложений Windows это «Не отвечает» — одна из самых частых жалоб. И что удивительно мало известно: то, что выставляет отображение «Не отвечает», — не само зависшее приложение, а ОС.

Как Windows узнаёт, что приложение «зависло»? Что это за матово-белое окно? Рассчитанная на разработчиков, которые пишут бизнес-приложения под Windows, и на ИТ-сотрудников, которые принимают заявки о зависающих приложениях, эта статья разбирает суждение «Не отвечает» от основ цикла сообщений и собирает классические причины зависаний, проекты, которые не зависают, и процедуру расследования момента зависания — всё опираясь на первичные источники.

1. Сначала вывод

  • «Не отвечает» — суждение ОС. Когда окно (и GUI-поток, которому оно принадлежит) не ждёт ввода, не находится в последовательности запуска и не извлекало сообщение (PeekMessage) 5 секунд, ОС трактует его как не отвечающее. Суждение не на процесс.1
  • Побелевшее окно — «окно-призрак». ОС скрыла исходное окно и подставила подделку той же позиции, размера и вида. Можно только перемещать, сворачивать или закрывать; содержимое не выполняется. Окно-призрак не создаётся, пока присоединён отладчик.2
  • Причина зависания почти всегда сводится к одному. UI-поток, который должен прокачивать цикл сообщений, заблокирован на тяжёлой работе или ожидании. Синхронный ввод-вывод, сетевые вызовы, ожидания блокировок и SendMessage между потоками — классика.3
  • Принцип проектирования — «не ждать и не считать на UI-потоке». Перенесите тяжёлую работу в рабочий поток (async/await + Task.Run в C#) и оставьте UI-поток посвящённым отрисовке, прогрессу и приёму отмены.4
  • DoEvents и ручная прокачка цикла сообщений — рассадник багов повторного входа. Отображение «Не отвечает» уходит, но структура теперь позволяет произвольным событиям прерывать работу посредине. Разделение, а не уклонение — правильный подход.
  • Расследование начинается с захвата состояния в момент зависания. Возьмите дамп и посмотрите стек UI-потока — и почти всегда можно опознать, чего он ждёт.

2. Предпосылка: приложения Windows движутся сообщениями

Чтобы понять «Не отвечает», сначала нужно принять, что GUI-приложение Windows управляется событиями. GUI-приложение не идёт само за вводом; оно получает сообщения, которые доставляет ОС (мышь, клавиатура, запросы перерисовки, таймеры и так далее), и действует по ним.3

У каждого потока, который создаёт окно, есть очередь сообщений, и он крутит цикл сообщений вроде такого.

MSG msg;
while (GetMessage(&msg, NULL, 0, 0) > 0) {
    TranslateMessage(&msg);
    DispatchMessage(&msg);   // the window procedure is called
}

GetMessage извлекает сообщение из очереди, а DispatchMessage вызывает оконную процедуру этого окна (функцию обработки сообщений). Обработка щелчка кнопки, перерисовка и обработчики событий WinForms или WPF — всё это, когда сводите к сути, выполняется внутри одной итерации этого цикла.5

Базовая структура цикла сообщенийОС кладёт мышь, клавиатуру и прочий ввод в очередь сообщений потока; цикл UI-потока извлекает его через GetMessage, вызывает оконную процедуру через DispatchMessage и возвращается к началу цикла, когда обработка законченаОС (ввод, запросы перерисовки, таймеры)Очередь сообщений потокаИзвлечь через GetMessageDispatchMessageОбработать в оконной процедуре

Рис. 1: Сердце GUI-приложения — цикл сообщений; каждый обработчик события выполняется как одна итерация этого цикла.

У этой структуры есть одно важное следствие. Если вы делаете долгую работу внутри оконной процедуры (обработчика события), цикл тем временем не может извлечь следующее сообщение. Он не может реагировать ни на щелчки, ни на запросы перерисовки — вот чем на самом деле является «зависание».

Стоит также принять, что сообщения доставляются двумя путями. PostMessage кладёт сообщение в очередь и сразу возвращается, и цикл извлекает и обрабатывает сообщения по порядку. SendMessage, с другой стороны, вызывает оконную процедуру напрямую и не возвращается вызывающему, пока обработка не завершится.36 Это различие напрямую переходит в обсуждение взаимной блокировки в главе 4.

Два пути доставки сообщенийPostMessage кладёт сообщение в очередь и сразу возвращается; цикл сообщений извлекает и обрабатывает сообщения по порядку. SendMessage вызывает оконную процедуру напрямую и не возвращается вызывающему, пока обработка не завершитсяPostMessageПоложить в очередь (сразу возвращается)Цикл извлекает и обрабатывает по порядкуSendMessageВызвать процедуру напрямуюНе возвращается, пока обработка не завершится

Рис. 2: Хотя оба «отправляют сообщение», поставленный в очередь Post и Send, который ждёт завершения, имеют совершенно разную природу.

3. Как судят «Не отвечает» — правило 5 секунд и окно-призрак

Так как же ОС узнаёт, что «это приложение зависло»? Критерий задокументирован официально. ОС трактует окно как не отвечающее, когда оно не ждёт ввода, не находится в последовательности запуска и не вызывало PeekMessage (извлечение сообщения) 5 секунд.1 Иными словами, ОС смотрит, «действительно ли крутится цикл сообщений», как вы бы брали пульс, и если пульса нет 5 секунд, судит окно не отвечающим (документация говорит, что это значение 5 секунд в будущем может измениться). Единица суждения — окно и GUI-поток, которому оно принадлежит; в приложении с несколькими UI-потоками зависание одного потока не значит, что окна другого потока мертвы. Поток, на который смотреть в дампе, — владелец зависшего окна.

Что происходит с окном верхнего уровня, которое осудили, тоже задокументировано. ОС скрывает исходное окно и подменяет его «окном-призраком» того же Z-порядка, позиции, размера и вида. Пользователь может только перемещать, менять размер или (принудительно) закрывать его. Приложение внутри на самом деле не отвечает, поэтому никакие другие операции не работают.2

Суждение о зависшем окне и подмена окном-призракомКогда UI-поток заблокирован на тяжёлой работе и извлечение сообщений останавливается на 5 секунд, ОС судит окно не отвечающим, скрывает исходное, подставляет окно-призрак того же вида и предлагает пользователю только перемещение, сворачивание и закрытиенетдаUI-поток заблокирован на тяжёлой работеИзвлечение сообщений останавливаетсяПрошло 5 секунд?Подставить окно-призракЗаголовок показывает (Не отвечает)Матово-белый; только перемещать и закрывать

Рис. 3: И текст «Не отвечает», и белый экран принадлежат окну-призраку, которое подставила ОС, а не зависшему приложению.

Строка «(Не отвечает)», которая появляется в заголовке, и матово-белый вид под темой Aero принадлежат этому окну-призраку. Следуют два практических следствия.

  • К моменту, когда отображается «Не отвечает», поток, которому принадлежит это окно, не обрабатывал сообщения как минимум 5 секунд. Дело не в том, что «отображение появилось слишком рано» — UI-поток определённо заблокирован.
  • Окно-призрак не создаётся, пока присоединён отладчик.2 Когда выглядит так, будто «под отладчиком никогда не уходит в „Не отвечает“, а в выпуске уходит», само зависание может быть тем же, а различается только отображение.

Есть также API DisableProcessWindowsGhosting, который отключает эту подмену для всего процесса.7 Он предназначен для особых случаев вроде киоск-терминалов, где вы не хотите, чтобы ОС сама делала окно выглядящим работоспособным. Вызов останавливает появление отображения «Не отвечает», но факт, что приложение зависло, не меняется. Поймите, что это не то, чем общее приложение пользуется как контрмерой против «Не отвечает».

4. Почему приложения зависают — классические шаблоны, которые блокируют UI-поток

Причина, сведённая к сути, — одна точка: «UI-поток не возвращается в цикл сообщений», — но формы, которые вы встречаете на практике, распадаются на несколько классических.

Классификация классических причин, которые блокируют UI-потокЧетыре классических семейства — синхронный ввод-вывод и сетевые вызовы, ожидания блокировок, SendMessage между потоками и участие COM STA — все сводятся к одной точке: UI-поток не может вернуться в цикл сообщенийКакая классическая причина?Ввод-вывод или блокировка?SendMessage или COM?Синхр. ввод-вывод и сетьОжидания блокировокSendMessageмежду потокамиУчастие COM STAUI не может вернутьсяПроверка «Не отвечает»

Рис. 4: Видимый симптом один, но виновник, блокирующий поток, распадается на четыре семейства, и контрмера у каждого своя.

Синхронный ввод-вывод и сетевые вызовы. Это самое частое. Шаблон делать синхронно внутри обработчика щелчка кнопки большое чтение или запись файла, запрос к базе данных, вызов веб-API или доступ к файлу на сетевом диске. На машине разработки это заканчивается за долю секунды, поэтому вы никогда не замечаете; производственная сетевая задержка или сбой файлового сервера превращает это в ожидание в десятки секунд, и вы получаете заявки, что «иногда зависает». У сетевых дисков длинные тайм-ауты, когда соединение мертво, и они драматически ухудшают симптом.

Ожидания блокировок. Шаблон, в котором UI-поток пытается взять блокировку на данные, общие с рабочим потоком, и в итоге ждёт рабочего, который держит эту блокировку долго. Дисциплина блокировок подробно разобрана в практической серии о многопоточности.

SendMessage между потоками. SendMessage не возвращается, пока процедура окна назначения не закончила обработку.6 Когда вы отправляете его окну на другом потоке, отправителя заставляют ждать, пока этот поток не окажется в состоянии, в котором может обрабатывать сообщения. Если поток назначения сам чего-то ждёт, получается взаимная блокировка сообщений, в которой каждая сторона ждёт другую.3 Отправка на HWND_BROADCAST в частности втянет вас, как только одно окно не отвечает. Когда ждать нельзя, рассмотрите SendMessageTimeout или PostMessage, который не ждёт ответа.8

Взаимная блокировка, вызванная SendMessage между потокамиЕсли рабочий поток отправляет SendMessage окну UI-потока, пока UI-поток заблокирован в ожидании результата рабочего, каждая сторона ждёт, пока другая закончит, и получается взаимная блокировкаРабочий потокUI-потокРабочий потокUI-потокНе может обрабатывать сообщения (заблокирован)Не может вернуться из SendMessageЖдут друг друга — взаимная блокировкаЖдёт завершения рабочего (заблокирован)SendMessage (не возвращается, пока не обработано)

Рис. 5: «UI-поток ждёт рабочего, а рабочий ждёт UI-поток через SendMessage» — классическая взаимная блокировка.

Участие COM-апартамента. Вызовы объекта STA доставляются как оконные сообщения, поэтому когда UI-поток (STA) заблокирован, вызовы COM из других потоков блокируются как побочный ущерб. Эта структура объяснена в статье о COM STA/MTA.

Накопление «это всего миг». Даже синхронный вызов на 50 мс, вызванный 100 раз в цикле, — 5 секунд. Порог «Не отвечает» — 5 секунд, но воспринимаемая «вялость» начинается около 100 мс. Практическое правило проектирования — «UI-поток можно блокировать только на миллисекунды».

5. Проекты, которые не зависают — уносим тяжёлую работу с UI-потока

Принцип проектирования один: уберите долгую работу с UI-потока. В C# (WinForms/WPF) async/await — самый короткий правильный подход.

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

Есть три пункта. Первый: пока await ждёт, UI-поток вернулся в цикл сообщений, поэтому вы не уходите в «Не отвечает». Второй: продолжение после await возвращается в UI-поток, поэтому потом можно касаться элементов управления как обычно (касаться элемента управления напрямую из рабочего потока запрещено; если нужно — используйте Control.Invoke / Dispatcher.InvokeAsync).4 Третий: отключайте кнопку, пока идёт работа, и иначе убивайте повторный вход проектом.

Картина та же в нативном Win32: отдайте работу рабочему потоку, уведомите UI-поток о завершении пользовательским сообщением через PostMessage и обновите UI в оконной процедуре. PostMessage только кладёт сообщение в очередь и сразу возвращается, поэтому сторона рабочего тоже не блокируется.6 Само ожидание рабочего потока пишите с дисциплиной, разобранной в статье об условных переменных.

Разделение ролей в приложении, которое не зависаетUI-поток отвечает только за приём ввода, показ прогресса и приём отмены; рабочий поток выполняет тяжёлую работу и возвращает завершение UI-потоку через PostMessage или продолжение awaitОтдать работуPostMessage / продолжение awaitUI-поток: ввод, прогресс, отменаРабочий поток: тяжёлая работаНикакого синхронного ввода-вывода или долгого счёта на UI-потоке

Рис. 6: Держите UI-поток как «приёмную», всегда отдавайте тяжёлую работу рабочему и берите только уведомление о завершении.

Чего стоит избегать — приём вставлять Application.DoEvents() или цикл PeekMessage между кусками тяжёлой работы только чтобы держать отображение живым. Вы обходите «Не отвечает», но произвольные обработчики событий повторно входят посреди работы. Второй щелчок кнопки, закрытие формы во время обработки, срабатывание таймера — любой из них может испортить данные, которые ещё обрабатываются, и баги зависят от момента и трудно воспроизвести. Держите ручную прокачку цикла сообщений внутри ограниченной структуры вроде модального диалога прогресса и как правило решайте разделением.

Временная шкала бага повторного входа, вызванного DoEventsВызов DoEvents посреди тяжёлой работы позволяет обработчику события поставленного в очередь щелчка прервать и выполниться, переписать данные, которые ещё обрабатываются, а затем возобновить исходную работу, давая порчу данных, зависящую от моментаUI-потокUI-потокОбработчик повторного щелчка кнопки прерываетТяжёлая работа начинается (данные обрабатываются)DoEvents (обработать сообщения в очереди)Прерывающая работа переписывает данныеИсходная работа возобновляется (данные уже несогласованы)

Рис. 7: DoEvents стирает «Не отвечает» ценой приглашения произвольных событий посреди работы.

Для долгой работы включайте в проект также отображение прогресса и отмену. Отправляйте прогресс в UI через IProgress<T> и сообщайте о прерывании через CancellationToken — и пользователь видит, что «оно работает», и не потянется к принудительному завершению (которое часто бывает причиной порчи данных).

Поток прогресса и отмены для долгой работыРабочий поток отправляет прогресс UI-потоку через IProgress; действие отмены на UI достигает рабочего через CancellationToken; рабочий останавливается на удобной границе и убирает за собойПрогресс через IProgressCancellationTokenРабочий: долгая работаUI: прогресс и кнопка СтопОстановиться на границе и убрать

Рис. 8: Прогресс — «рабочий → UI»; отмена — «UI → рабочий». Включите этот тонкий двусторонний канал в проект с самого начала.

6. Расследование момента зависания

В расследовании «иногда зависает» самое ценное — состояние потоков в точный момент зависания. Перезагрузите — и улики пропали.

Возьмите дамп. На вкладке «Подробности» Диспетчера задач щёлкните правой кнопкой целевой процесс → «Создать файл дампа». Этого одного достаточно, чтобы получить полный дамп со стеком каждого потока. Уже сказать ИТ-сотрудникам, которые принимают заявки, «когда зависнет, возьмите это, прежде чем закрывать» сильно меняет успешность расследования. О построении механизма сбора см. статью о сборе дампов сбоев.

Смотрите стек UI-потока. Откройте дамп в WinDbg и смотрите стек потока, который прокачивает цикл сообщений (обычно поток 0). Синхронный ввод-вывод виден как ReadFile или сетевой API, ожидание блокировки — как вызов семейства WaitFor…, а SendMessage между потоками — как ожидание внутри SendMessage — как есть. Как читать, объяснено во вводной статье о WinDbg.

Смотрите вживую. Через Process Explorer можно на месте осмотреть список потоков и стеки. Когда вялость устойчива, возьмите трассировку WPR и проанализируйте ожидания UI-потока во времени (WPR/WPA на практике).

Базовая процедура расследования «Не отвечает»Возьмите дамп в момент зависания, посмотрите стек UI-потока, опознайте, остановился ли он на синхронном вводе-выводе, ожидании блокировки или SendMessage между потоками, и свяжите это с соответствующим исправлением проектаМомент зависанияВзять дамп (до закрытия)Смотреть стек UI-потокаСинхронный ввод-вывод или сетевое ожиданиеОжидание блокировкиSendMessage между потокамиВынести место на рабочего

Рис. 9: Звезда расследования — «дамп момента зависания»; стек UI-потока сам по себе — классификация причины.

Можно также стандартизировать, как делать первый срез по симптому. Если всегда зависает на конкретной операции, сначала подозревайте синхронный ввод-вывод внутри этого обработчика. Если зависает редко и без связи с операцией, подозревайте порядок блокировок или взаимную блокировку SendMessage между потоками и сопоставьте цели ожидания обоих потоков в дампе. Если зависает только в конкретной среде, подозревайте тайм-ауты от факторов среды вроде сетевого диска, прокси или антивируса.

7. Итоги

  • «Не отвечает» — механизм, в котором ОС судит, что приложение не извлекало сообщение 5 секунд, и подставляет окно-призрак. То, что выставляет отображение, — ОС, а не приложение.
  • Причина зависания — одна точка: «UI-поток не может вернуться в цикл сообщений». Синхронный ввод-вывод, сеть, ожидания блокировок и SendMessage между потоками — классика.
  • Контрмера — унести тяжёлую работу с UI-потока. В C# — async/await + Task.Run; в Win32 — рабочий поток + PostMessage. Предотвращайте повторный вход во время выполнения проектом, например отключая кнопку.
  • Уклонение через DoEvents — ценой багов повторного входа. DisableProcessWindowsGhosting только снимает отображение. Ни то ни другое не исправление первопричины.
  • Для расследования дамп «момента зависания» — самое важное. Причина почти всегда написана на стеке UI-потока как есть.

С точки зрения пользователя «Не отвечает» — это «сломалось», но как только вы знаете механизм, можете перевести это в точное предложение «UI-поток не вернулся 5 секунд». Идя назад от этого одного предложения, кандидаты причин, исправление и процедура расследования все выпадают естественно.

Похожие статьи

Смежные области консультирования

KomuraSoft LLC занимается расследованием первопричин бизнес-приложений, которые «иногда зависают» или уходят в «Не отвечает» (анализ дампа и анализ трассировки), рефакторингом унаследованного UI-кода, полного синхронной работы, в разделение async/await и рабочих потоков, и ревью проектов UI, которые не замерзают. Даже когда у вас ещё нет процедуры воспроизведения, можем помочь начиная с проектирования того, как собирать улики.

Справочные ссылки

  1. Microsoft Learn, IsHungAppWindow function (winuser.h). О критерии суждения, по которому приложение трактуют как не отвечающее, когда оно «не ждёт ввода, не находится в последовательности запуска и не вызывало PeekMessage в течение внутреннего тайм-аута 5 секунд»; о том, что этот критерий 5 секунд может измениться; и о том, что функция для окна-призрака всегда возвращает TRUE.  2

  2. Microsoft Learn, GetMessage function (winuser.h). О том, что система трактует окно верхнего уровня как не отвечающее, когда оно несколько секунд не отвечает на сообщения, и подменяет его окном-призраком того же Z-порядка, позиции, размера и вида; о том, что пользователь может только перемещать, менять размер или закрывать его; и о том, что окно-призрак не создаётся, пока присоединён отладчик.  2 3

  3. Microsoft Learn, About Messages and Message Queues. О том, что приложения Windows управляются событиями и оконная процедура обрабатывает сообщения; о различии между сообщениями в очереди и сообщениями, отправленными напрямую; о подмене не отвечающего окна окном-призраком; и о разделе, покрывающем взаимную блокировку от потоков, отправляющих сообщения друг другу.  2 3 4

  4. Microsoft Learn, How to make thread-safe calls to controls (Windows Forms). О том, что элементы управления WinForms небезопасно касаться с любого потока, кроме того, который их создал; об использовании Invoke/BeginInvoke для обновлений с другого потока; и о безопасных асинхронных шаблонах через async/await или BackgroundWorker.  2

  5. Microsoft Learn, Using Messages and Message Queues. О типичной реализации цикла сообщений через GetMessage, TranslateMessage и DispatchMessage и о том, как осматривать очередь сообщений. 

  6. Microsoft Learn, SendMessage function (winuser.h). О том, что SendMessage вызывает оконную процедуру указанного окна и не возвращается, пока обработка не завершится; о том, что отправка окну на другом потоке заставляет отправителя ждать, пока этот поток обработает сообщение; и о различии с PostMessage, который кладёт сообщение в очередь, не ожидая ответа.  2 3

  7. Microsoft Learn, DisableProcessWindowsGhosting function (winuser.h). О возможности отключить для вызывающего GUI-процесса функцию окна-призрака, которая делает не отвечающее окно сворачиваемым, перемещаемым и закрываемым; и о том, что отключение длится всю жизнь процесса. 

  8. Microsoft Learn, SendMessageTimeout function (winuser.h). О возможности отправить сообщение с тайм-аутом; и о флаге (SMTO_ABORTIFHUNG), который возвращается, не ожидая, когда окно не отвечает (осуждено как зависшее). 

Недавние статьи с теми же тегами помогут подробнее изучить близкие темы.

Приложения, которые ломаются после выхода из сна — как работают события питания Windows и как строить бизнес-приложения, которые это переживают

Открыли ноутбук — и соединения бизнес-приложения мертвы: причина в проекте, который не учитывал сон. Статья разбирает поток уведомлений W...

Эти страницы показывают тему статьи в более широком контексте услуг и решений.

Статья напрямую связана со следующими услугами.

Частые вопросы

Вопросы, которые часто возникают при консультациях по теме статьи.

При каких условиях появляется «Не отвечает»?
ОС судит окно зависшим, когда оконное приложение не ждёт ввода, не находится в последовательности запуска и не извлекало сообщение (PeekMessage) 5 секунд. Зависшее окно верхнего уровня скрывают и подменяют «окном-призраком» той же позиции, размера и вида. Текст заголовка «(Не отвечает)» и матово-белый вид принадлежат этому окну-призраку, которое позволяет только перемещать, сворачивать или закрывать. Иными словами, «Не отвечает» — не то, что отображает само приложение: это экран, который ОС ставит от имени приложения.
Есть ли настройка, чтобы «Не отвечает» не появлялось, пока идёт работа?
Вызов DisableProcessWindowsGhosting отключает подмену окном-призраком для этого процесса. Это, однако, только делает зависание менее видимым пользователю — окно по-прежнему не реагирует на ввод, и с точки зрения пользователя это полная заморозка без способа переместить или закрыть. Настоящее исправление — не подавлять отображение, а перенести тяжёлую работу в рабочий поток, чтобы UI-поток никогда не блокировался даже на десятую долю секунды, не говоря о пяти. Заметьте также, что ОС не создаёт окно-призрак, пока присоединён отладчик, поэтому во время отладки может выглядеть так, будто «Не отвечает» никогда не случается.
Допустимо ли избегать «Не отвечает» через DoEvents (вручную прокачивая цикл сообщений)?
Не рекомендуется. Крутить DoEvents или цикл PeekMessage посреди тяжёлой работы обойдёт суждение о зависшем окне, но тогда любой обработчик события может повторно войти — второй щелчок кнопки, закрытие окна, таймер и так далее. Другой обработчик, переписывающий данные, которые ещё обрабатываются, или касающийся формы, которую предполагалось закрыть, и бросающий исключение, даёт баги повторного входа, которые зависят от момента и трудно воспроизвести — хуже, чем само «Не отвечает». Правильный подход — перенести саму работу в рабочий поток через Task.Run или подобное и оставить UI-потоку ответственность только за отображение прогресса и приём отмены.
Как обновлять UI (элементы управления) из рабочего потока?
Элементы управления WinForms и элементы WPF можно касаться только из потока, который их создал (обычно UI-поток). Касаться их напрямую из рабочего потока — значит вызывать исключения или неопределённое поведение. В C# самый простой путь — async/await: продолжение после await возвращается в вызывающий UI-поток, поэтому после await можно обновлять элементы управления как обычно. Чтобы переключиться явно, используйте Control.Invoke/BeginInvoke в WinForms и Dispatcher.InvokeAsync в WPF. В нативном Win32 устоявшийся шаблон — чтобы рабочий поток отправлял UI-потоку пользовательское сообщение о завершении через PostMessage, а оконная процедура обновляла UI.
Как расследовать, почему приложение показывает «Не отвечает»?
Важно захватить состояние в сам «момент» зависания. Сначала возьмите полный дамп с вкладки «Подробности» Диспетчера задач через «Создать файл дампа», затем в WinDbg смотрите стек UI-потока (потока, который крутит цикл сообщений). Застрял ли он в синхронном вводе-выводе, сетевом ожидании, ожидании блокировки или ожидании другого потока через SendMessage — видно на стеке как есть. Чтобы смотреть живой процесс, полезны список потоков и вид стека Process Explorer; чтобы проследить во времени, эффективно захватить трассировку WPR. См. также вводную статью этого сайта о WinDbg, Process Explorer на практике и WPR/WPA на практике.

Об авторе

Страница с профилем автора статьи.

Го Комура

Представитель KomuraSoft LLC

Специализируется на разработке программного обеспечения для Windows, техническом консалтинге и расследовании сбоев, особенно в проектах с унаследованными системами и трудно воспроизводимыми ошибками.

Публичные ссылки

Вернуться в блог