「응답 없음」의 정체 ── Windows가 앱의 행을 판단하는 방식과 멈추지 않는 설계

· · Windows, Windows 개발, 불량 조사, 멀티스레드, WinForms, WPF, Win32 API, UI 설계

「조작 도중에 앱이 하얘지며 (응답 없음)이 나온다」. 「가끔 멈춘다는 티켓이 오는데, 개발 머신에서는 한 번도 재현되지 않는다」── Windows 업무 앱에서 이 「응답 없음」은 가장 흔한 불만 중 하나입니다. 그리고 의외로 잘 알려지지 않은 것은, 「응답 없음」 표시를 올리는 것이 멈춘 앱 자신이 아니라── OS라는 점입니다.

Windows는 앱이 「멈췄다」는 것을 어떻게 아는가. 그 서리가 낀 흰 창은 무엇인가. Windows에서 업무 앱을 쓰는 개발자와, 멈추는 앱의 티켓을 받는 IT 담당자를 대상으로, 이 글은 메시지 루프의 기초부터 「응답 없음」 판단을 따라가고, 행의 고전적 원인, 멈추지 않는 설계, 멈춘 순간의 조사 절차를── 모두 1차 정보를 바탕으로 정리합니다.

1. 먼저 결론

  • 「응답 없음」은 OS의 판단입니다. 창(과 그것을 소유한 GUI 스레드)이 입력을 기다리지 않고, 기동 시퀀스에 있지 않으며, 5초 동안 메시지(PeekMessage)를 꺼내지 않으면 OS는 응답하지 않는다고 봅니다. 판단은 프로세스 단위가 아닙니다.1
  • 하얘진 창은 「고스트 창」입니다. OS가 원래 창을 숨기고 같은 위치·크기·외관의 가짜를 끼워 넣었습니다. 할 수 있는 일은 이동, 최소화, 닫기뿐이며, 내용은 돌고 있지 않습니다. 디버거가 붙어 있는 동안에는 고스트 창이 만들어지지 않습니다.2
  • 행의 원인은 거의 항상 하나로 귀결됩니다. 메시지 루프를 펌핑해야 할 UI 스레드가 무거운 작업이나 대기에서 막혀 있습니다. 동기 I/O, 네트워크 호출, 락 대기, 스레드를 넘는 SendMessage가 고전입니다.3
  • 설계 원칙은 「UI 스레드에서 기다리거나 계산하지 않는다」입니다. 무거운 작업은 워커 스레드로 옮기고(C#에서는 async/await + Task.Run) UI 스레드는 그리기, 진행, 취소 수락에 전념하게 합니다.4
  • DoEvents와 메시지 루프의 수동 펌핑은 재진입 버그의 온상입니다. 「응답 없음」 표시는 사라지지만, 구조상 임의의 이벤트가 작업 한가운데를 가로챌 수 있습니다. 회피가 아니라 분리가 올바른 접근입니다.
  • 조사는 멈춘 순간의 상태를 잡는 일에서 시작합니다. 덤프를 떠서 UI 스레드의 스택을 보면, 무엇을 기다리는지 거의 항상 식별할 수 있습니다.

2. 전제: Windows 앱은 메시지로 움직인다

「응답 없음」을 이해하려면, 먼저 Windows GUI 앱이 이벤트 구동이라는 점을 받아들여야 합니다. GUI 앱은 입력을 스스로 가지러 가지 않고, OS가 전달하는 메시지(마우스, 키보드, 다시 그리기 요청, 타이머 등)를 받아 그에 따라 움직입니다.3

창을 만드는 각 스레드는 메시지 큐를 갖고, 다음과 같은 메시지 루프를 돕니다.

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

GetMessage가 큐에서 메시지를 꺼내고, DispatchMessage가 그 창의 윈도우 프로시저(메시지 처리 함수)를 호출합니다. 버튼 클릭 처리, 다시 그리기, WinForms나 WPF 이벤트 핸들러는 모두, 끓여 보면 이 루프의 한 반복 안에서 돕니다.5

메시지 루프의 기본 구조OS가 마우스, 키보드 등의 입력을 스레드의 메시지 큐에 넣고, UI 스레드의 루프가 GetMessage로 꺼내 DispatchMessage로 윈도우 프로시저를 호출하며, 처리가 끝나면 루프 꼭대기로 돌아온다OS(입력, 다시 그리기 요청, 타이머)스레드 메시지 큐GetMessage로 꺼낸다DispatchMessage윈도우 프로시저에서 처리

그림 1: GUI 앱의 심장은 메시지 루프이며, 모든 이벤트 핸들러는 이 루프의 한 반복으로 돈다.

이 구조에는 중요한 귀결이 하나 있습니다. 윈도우 프로시저(이벤트 핸들러) 안에서 시간이 걸리는 작업을 하면, 그동안 루프는 다음 메시지를 꺼낼 수 없습니다. 클릭에도, 다시 그리기 요청에도 반응할 수 없습니다── 그것이 「행」의 정체입니다.

메시지가 두 경로로 전달된다는 점도 받아들여 둘 가치가 있습니다. PostMessage메시지를 큐에 놓고 즉시 돌아오며, 루프가 메시지를 순서대로 꺼내 처리합니다. 한편 SendMessage윈도우 프로시저를 직접 호출하고, 처리가 끝날 때까지 호출자에게 돌아오지 않습니다.36 그 차이는 4장의 데드락 논의로 바로 이어집니다.

두 가지 메시지 전달 경로PostMessage는 메시지를 큐에 놓고 즉시 돌아오며, 메시지 루프가 순서대로 꺼내 처리한다. SendMessage는 프로시저를 직접 호출하고 처리가 끝날 때까지 호출자에게 돌아오지 않는다PostMessage큐에 놓는다(즉시 돌아온다)루프가 순서대로 꺼내 처리한다SendMessage프로시저를 직접 호출한다처리가 끝날 때까지 돌아오지 않는다

그림 2: 둘 다 「메시지를 보낸다」지만, 큐에 넣는 Post와 완료를 기다리는 Send는 성질이 완전히 다르다.

3. 「응답 없음」은 어떻게 판단되는가 ── 5초 규칙과 고스트 창

그렇다면 OS는 「이 앱이 멈췄다」를 어떻게 아는가. 기준은 공식으로 문서화되어 있습니다. OS는 창이 입력을 기다리지 않고, 기동 시퀀스에 있지 않으며, 5초 동안 PeekMessage(메시지 꺼내기)를 호출하지 않았을 때 응답하지 않는다고 봅니다.1 즉 OS는 「메시지 루프가 실제로 돌고 있는지」를 맥박을 재듯 보고, 5초 동안 맥박이 없으면 창이 응답하지 않는다고 판단합니다(문서는 이 5초 값이 미래에 바뀔 수 있다고 말합니다). 판단의 단위는 창과 그것을 소유한 GUI 스레드이며, UI 스레드가 여러 개인 앱에서 한 스레드가 멈춘다고 다른 스레드의 창이 죽은 것은 아닙니다. 덤프에서 볼 스레드는 멈춘 창의 소유자입니다.

판단된 최상위 창에 일어나는 일도 문서화되어 있습니다. OS는 원래 창을 숨기고, 같은 Z 순서·위치·크기·외관의 「고스트 창」으로 바꿉니다. 사용자가 할 수 있는 일은 이동, 크기 변경, (강제) 닫기뿐입니다. 안의 앱은 실제로 응답하지 않으므로 다른 조작은 통하지 않습니다.2

행 창 판단과 고스트 창 교체UI 스레드가 무거운 작업에서 막혀 메시지 꺼내기가 5초 멈추면, OS는 창이 응답하지 않는다고 판단하고 원본을 숨긴 뒤 같은 외관의 고스트 창을 끼워 넣어, 사용자에게는 이동·최소화·닫기만 제공한다아니오무거운 작업에서 막힌 UI 스레드메시지 꺼내기가 멈춘다5초가 지났는가?고스트 창으로 교체제목에 (응답 없음)이 나온다서리가 낀 흰색, 이동과 닫기만

그림 3: 「응답 없음」 문자열과 흰 화면은 모두 OS가 끼워 넣은 고스트 창의 것이며, 멈춘 앱의 것이 아니다.

제목 표시줄에 나타나는 「(응답 없음)」 문자열과, Aero 테마 아래의 서리가 낀 흰 모습은 모두 이 고스트 창의 것입니다. 실무상의 귀결이 둘 따라옵니다.

  • 「응답 없음」이 표시된 시점에는, 그 창을 소유한 스레드가 최소 5초 동안 메시지를 처리하지 않은 것입니다. 「표시가 너무 일찍 나왔다」가 아니라── UI 스레드는 분명히 막혀 있습니다.
  • 디버거가 붙어 있는 동안에는 고스트 창이 만들어지지 않습니다.2 「디버거 아래에서는 응답 없음이 되지 않는데, 릴리스에서는 된다」처럼 보이면, 행 자체는 같고 표시만 다를 수 있습니다.

이 교체를 프로세스 전체에서 끄는 API, DisableProcessWindowsGhosting도 있습니다.7 키오스크 단말처럼 OS가 혼자 창을 조작 가능해 보이게 하지 않기를 원하는 특수한 경우를 위한 것입니다. 호출하면 「응답 없음」 표시는 나오지 않지만, 앱이 멈춘 사실은 바뀌지 않습니다. 일반 앱이 「응답 없음」 대책으로 쓰는 것이 아님을 이해하십시오.

4. 앱이 멈추는 이유 ── UI 스레드를 막는 고전적 패턴

원인은 끓여 보면 한 점── 「UI 스레드가 메시지 루프로 돌아오지 않는다」── 이지만, 실무에서 만나는 형태는 몇 가지 고전으로 나뉩니다.

UI 스레드를 막는 고전적 원인의 분류동기 I/O와 네트워크 호출, 락 대기, 스레드를 넘는 SendMessage, COM STA 관여라는 네 고전 계열은 모두 UI 스레드가 메시지 루프로 돌아올 수 없다는 같은 한 점으로 귀결된다어느 고전적 원인인가?I/O인가 락인가?SendMessage인가 COM인가?동기 I/O와 네트워크락 대기SendMessage스레드를 넘어COM STA 관여UI가 돌아올 수 없다응답 없음 검사

그림 4: 보이는 증상은 같지만, 스레드를 막는 범인은 네 계열이며 대책은 각각 다르다.

동기 I/O와 네트워크 호출. 이것이 가장 흔합니다. 버튼 클릭 핸들러 안에서 큰 파일 읽기/쓰기, 데이터베이스 질의, Web API 호출, 네트워크 드라이브의 파일 접근을 동기로 하는 패턴입니다. 개발 머신에서는 1초의 몇 분의 일에 끝나 알아차리지 못하고, 운영 네트워크 지연이나 파일 서버의 딸꾹질이 수십 초의 대기가 되어 「가끔 멈춘다」는 티켓이 옵니다. 네트워크 드라이브는 연결이 끊겼을 때 타임아웃이 길어, 증상을 극적으로 악화시킵니다.

락 대기. UI 스레드가 워커 스레드와 공유하는 데이터의 락을 잡으려다, 그 락을 오래 쥐고 있는 워커를 기다리게 되는 패턴입니다. 락의 규율은 멀티스레드 실무 연재에서 자세히 다룹니다.

스레드를 넘는 SendMessage. SendMessage대상 창의 프로시저가 처리를 끝낼 때까지 돌아오지 않습니다.6 다른 스레드의 창에 보내면, 그 스레드가 메시지를 처리할 수 있는 상태가 될 때까지 송신 측이 기다리게 됩니다. 대상 스레드 자신이 무언가를 기다리고 있으면, 서로가 서로를 기다리는 메시지 데드락이 됩니다.3 특히 HWND_BROADCAST로 보내면 창 하나만 응답하지 않아도 끌려들어 갑니다. 기다릴 여유가 없으면 응답을 기다리지 않는 SendMessageTimeout이나 PostMessage를 검토하십시오.8

스레드를 넘는 SendMessage가 일으키는 데드락UI 스레드가 워커의 결과를 기다리며 막혀 있는 동안 워커 스레드가 UI 스레드 창에 SendMessage를 보내면, 서로가 상대의 완료를 기다려 데드락이 된다워커 스레드UI 스레드워커 스레드UI 스레드메시지를 처리할 수 없음(막힘)SendMessage에서 돌아올 수 없음서로를 기다림 ── 데드락워커가 끝나기를 기다림(막힘)SendMessage(처리될 때까지 돌아오지 않음)

그림 5: 「UI 스레드가 워커를 기다리고, 워커가 SendMessage로 UI 스레드를 기다린다」는 고전적 데드락이다.

COM 아파트먼트 관여. STA 객체에 대한 호출은 창 메시지로 전달되므로, UI 스레드(STA)가 막히면 다른 스레드에서의 COM 호출도 부수적으로 막힙니다. 그 구조는 COM STA/MTA 기사에서 설명합니다.

「잠깐이면 된다」의 쌓임. 50ms의 동기 호출도 루프에서 100번 부르면 5초입니다. 응답 없음의 임계값은 5초이지만, 체감의 「버벅임」은 100ms 근처에서 시작합니다. 설계의 경험 규칙은 「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에서도 그림은 같습니다. 작업을 워커 스레드에 넘기고, 완료를 PostMessage로 사용자 정의 메시지로 UI 스레드에 알리며, 윈도우 프로시저에서 UI를 갱신합니다. PostMessage는 메시지를 큐에 놓고 즉시 돌아오므로, 워커 측도 막히지 않습니다.6 워커 스레드 자체를 기다리는 일은 조건 변수 기사에서 다룬 규율로 쓰십시오.

멈추지 않는 앱의 역할 분담UI 스레드는 입력 수락, 진행 표시, 취소 수락만 담당하고, 워커 스레드가 무거운 작업을 돌린 뒤 PostMessage 또는 await 연속으로 완료를 UI 스레드에 되돌린다작업을 넘긴다PostMessage / await 연속UI 스레드: 입력, 진행, 취소워커 스레드: 무거운 작업UI 스레드에서 동기 I/O나 긴 계산을 하지 않는다

그림 6: UI 스레드는 「접수 창구」로 두고, 무거운 작업은 항상 워커에 넘기며, 완료 알림만 받는다.

피하고 싶은 것은 표시를 살리려고 무거운 작업 덩어리 사이에 Application.DoEvents()PeekMessage 루프를 끼워 넣는 기법입니다. 응답 없음은 피하지만, 임의의 이벤트 핸들러가 작업 한가운데에 재진입합니다. 버튼의 두 번째 클릭, 처리 중의 폼 닫기, 타이머 발화── 어느 것이든 아직 처리 중인 데이터를 망가뜨릴 수 있고, 버그는 타이밍 의존이며 재현이 어렵습니다. 메시지 루프의 수동 펌핑은 모달 진행 대화상자 같은 한정된 구조 안에 두고, 원칙적으로는 분리로 해결하십시오.

DoEvents가 일으키는 재진입 버그의 타임라인무거운 작업 한가운데에서 DoEvents를 호출하면 큐에 있던 클릭의 이벤트 핸들러가 끼어들어 아직 처리 중인 데이터를 다시 쓴 뒤 원래 작업이 재개되어, 타이밍 의존의 데이터 파손이 난다UI 스레드UI 스레드버튼 재클릭 핸들러가 끼어든다무거운 작업 시작(처리 중인 데이터)DoEvents(큐의 메시지를 처리)끼어든 작업이 데이터를 다시 쓴다원래 작업이 재개된다(데이터는 이미 불일치)

그림 7: DoEvents는 「응답 없음」을 지우는 대가로, 작업 한가운데에 임의의 이벤트를 불러들인다.

오래 도는 작업에는 진행 표시와 취소도 설계에 넣습니다. 진행은 IProgress<T>로 UI에 보내고, 중단은 CancellationToken으로 전달하면, 사용자는 「돌아가고 있다」는 것을 보고 강제 종료에 손을 대지 않습니다(강제 종료는 종종 데이터 파손의 원인입니다).

오래 도는 작업의 진행과 취소 흐름워커 스레드는 IProgress로 진행을 UI 스레드에 보내고, UI의 취소 동작은 CancellationToken으로 워커에 닿으며, 워커는 편리한 경계에서 멈추고 정리한다IProgress로 진행CancellationToken워커: 오래 도는 작업UI: 진행과 중지 버튼경계에서 멈추고 정리한다

그림 8: 진행은 「워커 → UI」, 취소는 「UI → 워커」이다. 이 얇은 양방향 채널을 처음부터 설계에 넣는다.

6. 멈춘 순간의 조사

「가끔 멈춘다」는 조사에서 가장 가치 있는 것은 바로 그 멈춘 순간의 스레드 상태입니다. 재부팅하면 증거는 사라집니다.

덤프를 뜹니다. 작업 관리자의 세부 정보 탭에서 대상 프로세스를 오른쪽 클릭 → 「덤프 파일 만들기」. 그것만으로 모든 스레드의 스택이 들어 있는 전체 덤프를 얻습니다. 티켓을 받는 IT 담당자에게 「멈췄을 때, 닫기 전에 이것을 떠 달라」고 말하는 것만으로 조사 성공률이 크게 바뀝니다. 수집 메커니즘을 만드는 일은 크래시 덤프 수집 기사를 보십시오.

UI 스레드의 스택을 봅니다. 덤프를 WinDbg로 열고 메시지 루프를 펌핑하는 스레드(보통 스레드 0)의 스택을 봅니다. 동기 I/O는 ReadFile이나 네트워크 API로, 락 대기는 WaitFor… 계열 호출로, 스레드를 넘는 SendMessageSendMessage 안의 대기로── 그대로 나옵니다. 읽는 방법은 WinDbg 입문 기사에서 설명합니다.

살아 있는 채로 봅니다. Process Explorer로는 스레드 목록과 스택을 그 자리에서 검사할 수 있습니다. 꾸준히 버벅일 때는 WPR 트레이스를 떠서 UI 스레드의 대기를 시간에 따라 분석합니다(WPR/WPA in Practice).

응답 없음 조사의 기본 절차멈춘 순간에 덤프를 뜨고 UI 스레드의 스택을 보아, 동기 I/O, 락 대기, 스레드를 넘는 SendMessage 중 어디에 멈췄는지 식별한 뒤, 맞는 설계 수정으로 연결한다멈춘 순간덤프를 뜬다(닫기 전에)UI 스레드의 스택을 본다동기 I/O 또는 네트워크 대기락 대기스레드를 넘는 SendMessage해당 지점을 워커로 분리한다

그림 9: 조사의 주역은 「멈춘 순간의 덤프」이며, UI 스레드의 스택 자체가 원인의 분류이다.

증상에서 첫 절단을 내는 방법도 표준화할 수 있습니다. 특정 조작에서 항상 멈추면, 먼저 그 핸들러 안의 동기 I/O를 의심합니다. 조작과 상관 없이 드물게 멈추면, 락 순서나 스레드를 넘는 SendMessage 데드락을 의심하고, 덤프에서 양쪽 스레드의 대기 대상을 맞춥니다. 특정 환경에서만 멈추면, 네트워크 드라이브, 프록시, 백신 같은 환경 요인의 타임아웃을 의심합니다.

7. 정리

  • 「응답 없음」은 OS가 앱이 5초 동안 메시지를 꺼내지 않았다고 판단해 고스트 창을 끼워 넣는 메커니즘입니다. 표시를 올리는 것은 앱이 아니라 OS입니다.
  • 행의 원인은 한 점입니다. 「UI 스레드가 메시지 루프로 돌아올 수 없다」. 동기 I/O, 네트워크, 락 대기, 스레드를 넘는 SendMessage가 고전입니다.
  • 대책은 무거운 작업을 UI 스레드에서 떼어 내는 것입니다. C#에서는 async/await + Task.Run, Win32에서는 워커 스레드 + PostMessage. 실행 중의 재진입은 버튼을 끄는 등으로 설계로 막습니다.
  • DoEvents로 피하는 것은 재진입 버그와의 교환입니다. DisableProcessWindowsGhosting은 표시만 없앱니다. 둘 다 근본 수정이 아닙니다.
  • 조사에서는 「멈춘 순간」의 덤프가 가장 중요합니다. 원인은 거의 항상 UI 스레드의 스택에 그대로 쓰여 있습니다.

사용자의 관점에서 「응답 없음」은 「고장났다」이지만, 메커니즘을 알면 「UI 스레드가 5초 동안 돌아오지 않았다」는 정확한 문장으로 번역할 수 있습니다. 그 한 문장에서 거꾸로 가면, 후보 원인, 수정, 조사 절차가 모두 자연스럽게 나옵니다.

관련 기사

관련 상담 영역

합동회사 코무라소프트에서는 「가끔 멈춘다」, 「응답 없음」이 되는 업무 앱의 원인 조사(덤프 분석과 트레이스 분석), 동기 작업으로 가득한 레거시 UI 코드를 async/await와 워커 스레드 분리로 리팩터하는 작업, 멈추지 않는 UI 설계의 리뷰를 다루고 있습니다. 아직 재현 절차가 없어도, 증거를 모으는 설계부터 도울 수 있습니다.

참고 링크

  1. Microsoft Learn, IsHungAppWindow function (winuser.h). 앱이 「입력을 기다리지 않고, 기동 시퀀스에 있지 않으며, 내부 타임아웃 5초 동안 PeekMessage를 호출하지 않았을 때」 응답하지 않는다고 보는 판단 기준, 이 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)에 대해. 

같은 태그를 공유하는 최신 기사입니다. 더 가까운 주제로 지식을 넓힐 수 있습니다.

이 기사와 가까운 토픽 페이지입니다. 기사를 출발점 삼아 관련 서비스와 다른 기사로 이어집니다.

이 기사는 다음 서비스 페이지로 이어집니다. 가까운 입구부터 확인해 주세요.

자주 묻는 질문

이 기사 주제에 대해 상담 시 자주 나오는 질문을 모았습니다.

「응답 없음」은 어떤 조건에서 나타나나요?
OS는 창이 있는 앱이 입력을 기다리지 않고, 기동 시퀀스에 있지 않으며, 5초 동안 메시지(PeekMessage)를 꺼내지 않았을 때 그 창을 행으로 판단합니다. 행이 난 최상위 창은 숨겨지고, 같은 위치·크기·외관의 「고스트 창」으로 바뀝니다. 「(응답 없음)」 제목 표시줄 문자열과 서리가 낀 흰 모습은 이 고스트 창의 것이며, 이동·최소화·닫기만 할 수 있습니다. 즉 「응답 없음」은 앱 자신이 표시하는 것이 아니라, OS가 앱을 대신해 올리는 화면입니다.
작업 중에 「응답 없음」이 나오지 않게 하는 설정이 있나요?
DisableProcessWindowsGhosting을 호출하면 그 프로세스에서 고스트 창으로의 교체가 꺼집니다. 다만 그것은 행을 사용자에게 덜 보이게 할 뿐이며── 창은 여전히 입력에 반응하지 않고, 사용자 관점에서는 이동도 닫기도 할 수 없는 완전한 정지입니다. 진짜 수정은 표시를 억제하는 것이 아니라, 무거운 작업을 워커 스레드로 옮겨 UI 스레드가 5초는커녕 0.1초도 막히지 않게 하는 것입니다. 또한 디버거가 붙어 있는 동안 OS는 고스트 창을 만들지 않으므로, 디버깅 중에는 「응답 없음」이 일어나지 않는 것처럼 보일 수 있습니다.
DoEvents(메시지 루프를 수동으로 펌핑)로 「응답 없음」을 피해도 되나요?
권장하지 않습니다. 무거운 작업 한가운데에서 DoEvents나 PeekMessage 루프를 돌리면 행 창 판단은 피하지만, 그때 임의의 이벤트 핸들러가 재진입할 수 있습니다── 버튼의 두 번째 클릭, 창 닫기, 타이머 등. 아직 처리 중인 데이터를 다른 핸들러가 다시 쓰거나, 닫혀야 할 폼을 건드려 예외가 나는, 타이밍 의존이고 재현이 어려운 재진입 버그가 「응답 없음」보다 더 나쁩니다. 올바른 접근은 Task.Run 등으로 작업 자체를 워커 스레드로 옮기고, UI 스레드는 진행 표시와 취소 수락만 담당하게 하는 것입니다.
워커 스레드에서 UI(컨트롤)를 어떻게 갱신하나요?
WinForms 컨트롤과 WPF 요소는 그것을 만든 스레드(보통 UI 스레드)에서만 건드릴 수 있습니다. 워커 스레드에서 직접 건드리면 예외나 미정의 동작이 납니다. C#에서는 async/await가 가장 쉬운 길입니다. await 뒤의 연속은 호출한 UI 스레드로 돌아오므로, await 뒤에 컨트롤을 보통처럼 갱신할 수 있습니다. 명시적으로 전환하려면 WinForms에서는 Control.Invoke/BeginInvoke, WPF에서는 Dispatcher.InvokeAsync를 씁니다. 네이티브 Win32에서는 워커 스레드가 사용자 정의 완료 메시지를 UI 스레드에 PostMessage하고, 윈도우 프로시저가 UI를 갱신하는 것이 정석입니다.
앱이 「응답 없음」을 보이는 이유는 어떻게 조사하나요?
중요한 것은 행난 「바로 그 순간」의 상태를 잡는 것입니다. 먼저 작업 관리자의 세부 정보 탭에서 「덤프 파일 만들기」로 전체 덤프를 뜬 뒤, WinDbg에서 UI 스레드(메시지 루프를 도는 스레드)의 스택을 봅니다. 동기 I/O, 네트워크 대기, 락 대기, SendMessage를 통한 다른 스레드 대기인지는 스택에 그대로 나옵니다. 살아 있는 프로세스를 보려면 Process Explorer의 스레드 목록과 스택 보기가 유용하고, 시간에 따라 따라가려면 WPR 트레이스를 잡는 것이 효과적입니다. 이 사이트의 WinDbg 입문 기사, Process Explorer 실전, WPR/WPA 실전도 보십시오.

저자 프로필

기사 저자의 프로필 페이지입니다.

Go Komura

합동회사 코무라소프트 대표

Windows 소프트웨어 개발, 기술 상담, 장애 조사를 중심으로 재현이 어려운 장애 조사와 기존 자산이 남아 있는 프로젝트에 강점이 있습니다.

블로그 목록으로 돌아가기