「조작 도중에 앱이 하얘지며 (응답 없음)이 나온다」. 「가끔 멈춘다는 티켓이 오는데, 개발 머신에서는 한 번도 재현되지 않는다」── 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
flowchart TB
accTitle: 메시지 루프의 기본 구조
accDescr: OS가 마우스, 키보드 등의 입력을 스레드의 메시지 큐에 넣고, UI 스레드의 루프가 GetMessage로 꺼내 DispatchMessage로 윈도우 프로시저를 호출하며, 처리가 끝나면 루프 꼭대기로 돌아온다
os["OS(입력, 다시 그리기 요청, 타이머)"] --> q["스레드 메시지 큐"]
q --> gm["GetMessage로 꺼낸다"]
gm --> dm["DispatchMessage"]
dm --> wp["윈도우 프로시저에서 처리"]
wp --> gm
그림 1: GUI 앱의 심장은 메시지 루프이며, 모든 이벤트 핸들러는 이 루프의 한 반복으로 돈다.
이 구조에는 중요한 귀결이 하나 있습니다. 윈도우 프로시저(이벤트 핸들러) 안에서 시간이 걸리는 작업을 하면, 그동안 루프는 다음 메시지를 꺼낼 수 없습니다. 클릭에도, 다시 그리기 요청에도 반응할 수 없습니다── 그것이 「행」의 정체입니다.
메시지가 두 경로로 전달된다는 점도 받아들여 둘 가치가 있습니다. PostMessage는 메시지를 큐에 놓고 즉시 돌아오며, 루프가 메시지를 순서대로 꺼내 처리합니다. 한편 SendMessage는 윈도우 프로시저를 직접 호출하고, 처리가 끝날 때까지 호출자에게 돌아오지 않습니다.36 그 차이는 4장의 데드락 논의로 바로 이어집니다.
flowchart TB
accTitle: 두 가지 메시지 전달 경로
accDescr: PostMessage는 메시지를 큐에 놓고 즉시 돌아오며, 메시지 루프가 순서대로 꺼내 처리한다. SendMessage는 프로시저를 직접 호출하고 처리가 끝날 때까지 호출자에게 돌아오지 않는다
pm["PostMessage"] --> q2["큐에 놓는다(즉시 돌아온다)"]
q2 --> loop["루프가 순서대로 꺼내 처리한다"]
sm["SendMessage"] --> direct["프로시저를 직접 호출한다"]
direct --> w2["처리가 끝날 때까지 돌아오지 않는다"]
그림 2: 둘 다 「메시지를 보낸다」지만, 큐에 넣는 Post와 완료를 기다리는 Send는 성질이 완전히 다르다.
3. 「응답 없음」은 어떻게 판단되는가 ── 5초 규칙과 고스트 창
그렇다면 OS는 「이 앱이 멈췄다」를 어떻게 아는가. 기준은 공식으로 문서화되어 있습니다. OS는 창이 입력을 기다리지 않고, 기동 시퀀스에 있지 않으며, 5초 동안 PeekMessage(메시지 꺼내기)를 호출하지 않았을 때 응답하지 않는다고 봅니다.1 즉 OS는 「메시지 루프가 실제로 돌고 있는지」를 맥박을 재듯 보고, 5초 동안 맥박이 없으면 창이 응답하지 않는다고 판단합니다(문서는 이 5초 값이 미래에 바뀔 수 있다고 말합니다). 판단의 단위는 창과 그것을 소유한 GUI 스레드이며, UI 스레드가 여러 개인 앱에서 한 스레드가 멈춘다고 다른 스레드의 창이 죽은 것은 아닙니다. 덤프에서 볼 스레드는 멈춘 창의 소유자입니다.
판단된 최상위 창에 일어나는 일도 문서화되어 있습니다. OS는 원래 창을 숨기고, 같은 Z 순서·위치·크기·외관의 「고스트 창」으로 바꿉니다. 사용자가 할 수 있는 일은 이동, 크기 변경, (강제) 닫기뿐입니다. 안의 앱은 실제로 응답하지 않으므로 다른 조작은 통하지 않습니다.2
flowchart TB
accTitle: 행 창 판단과 고스트 창 교체
accDescr: UI 스레드가 무거운 작업에서 막혀 메시지 꺼내기가 5초 멈추면, OS는 창이 응답하지 않는다고 판단하고 원본을 숨긴 뒤 같은 외관의 고스트 창을 끼워 넣어, 사용자에게는 이동·최소화·닫기만 제공한다
busy["무거운 작업에서 막힌 UI 스레드"] --> stop["메시지 꺼내기가 멈춘다"]
stop --> judge{"5초가 지났는가?"}
judge -->|"아니오"| stop
judge -->|"예"| ghost["고스트 창으로 교체"]
ghost --> u1["제목에 (응답 없음)이 나온다"]
ghost --> u2["서리가 낀 흰색, 이동과 닫기만"]
그림 3: 「응답 없음」 문자열과 흰 화면은 모두 OS가 끼워 넣은 고스트 창의 것이며, 멈춘 앱의 것이 아니다.
제목 표시줄에 나타나는 「(응답 없음)」 문자열과, Aero 테마 아래의 서리가 낀 흰 모습은 모두 이 고스트 창의 것입니다. 실무상의 귀결이 둘 따라옵니다.
- 「응답 없음」이 표시된 시점에는, 그 창을 소유한 스레드가 최소 5초 동안 메시지를 처리하지 않은 것입니다. 「표시가 너무 일찍 나왔다」가 아니라── UI 스레드는 분명히 막혀 있습니다.
- 디버거가 붙어 있는 동안에는 고스트 창이 만들어지지 않습니다.2 「디버거 아래에서는 응답 없음이 되지 않는데, 릴리스에서는 된다」처럼 보이면, 행 자체는 같고 표시만 다를 수 있습니다.
이 교체를 프로세스 전체에서 끄는 API, DisableProcessWindowsGhosting도 있습니다.7 키오스크 단말처럼 OS가 혼자 창을 조작 가능해 보이게 하지 않기를 원하는 특수한 경우를 위한 것입니다. 호출하면 「응답 없음」 표시는 나오지 않지만, 앱이 멈춘 사실은 바뀌지 않습니다. 일반 앱이 「응답 없음」 대책으로 쓰는 것이 아님을 이해하십시오.
4. 앱이 멈추는 이유 ── UI 스레드를 막는 고전적 패턴
원인은 끓여 보면 한 점── 「UI 스레드가 메시지 루프로 돌아오지 않는다」── 이지만, 실무에서 만나는 형태는 몇 가지 고전으로 나뉩니다.
flowchart TB
accTitle: UI 스레드를 막는 고전적 원인의 분류
accDescr: 동기 I/O와 네트워크 호출, 락 대기, 스레드를 넘는 SendMessage, COM STA 관여라는 네 고전 계열은 모두 UI 스레드가 메시지 루프로 돌아올 수 없다는 같은 한 점으로 귀결된다
kind{"어느 고전적 원인인가?"}
kind --> io{"I/O인가 락인가?"}
kind --> other{"SendMessage인가 COM인가?"}
io --> c1["동기 I/O와 네트워크"]
io --> c2["락 대기"]
other --> c3["SendMessage"]
c3 -.-> c3n["스레드를 넘어"]
other --> c4["COM STA 관여"]
c1 --> core["UI가 돌아올 수 없다"]
c2 --> core
c3 --> core
c4 --> core
core --> ar["응답 없음 검사"]
그림 4: 보이는 증상은 같지만, 스레드를 막는 범인은 네 계열이며 대책은 각각 다르다.
동기 I/O와 네트워크 호출. 이것이 가장 흔합니다. 버튼 클릭 핸들러 안에서 큰 파일 읽기/쓰기, 데이터베이스 질의, Web API 호출, 네트워크 드라이브의 파일 접근을 동기로 하는 패턴입니다. 개발 머신에서는 1초의 몇 분의 일에 끝나 알아차리지 못하고, 운영 네트워크 지연이나 파일 서버의 딸꾹질이 수십 초의 대기가 되어 「가끔 멈춘다」는 티켓이 옵니다. 네트워크 드라이브는 연결이 끊겼을 때 타임아웃이 길어, 증상을 극적으로 악화시킵니다.
락 대기. UI 스레드가 워커 스레드와 공유하는 데이터의 락을 잡으려다, 그 락을 오래 쥐고 있는 워커를 기다리게 되는 패턴입니다. 락의 규율은 멀티스레드 실무 연재에서 자세히 다룹니다.
스레드를 넘는 SendMessage. SendMessage는 대상 창의 프로시저가 처리를 끝낼 때까지 돌아오지 않습니다.6 다른 스레드의 창에 보내면, 그 스레드가 메시지를 처리할 수 있는 상태가 될 때까지 송신 측이 기다리게 됩니다. 대상 스레드 자신이 무언가를 기다리고 있으면, 서로가 서로를 기다리는 메시지 데드락이 됩니다.3 특히 HWND_BROADCAST로 보내면 창 하나만 응답하지 않아도 끌려들어 갑니다. 기다릴 여유가 없으면 응답을 기다리지 않는 SendMessageTimeout이나 PostMessage를 검토하십시오.8
sequenceDiagram
accTitle: 스레드를 넘는 SendMessage가 일으키는 데드락
accDescr: UI 스레드가 워커의 결과를 기다리며 막혀 있는 동안 워커 스레드가 UI 스레드 창에 SendMessage를 보내면, 서로가 상대의 완료를 기다려 데드락이 된다
participant U as UI 스레드
participant W as 워커 스레드
U->>U: 워커가 끝나기를 기다림(막힘)
W->>U: SendMessage(처리될 때까지 돌아오지 않음)
Note over U: 메시지를 처리할 수 없음(막힘)
Note over W: SendMessage에서 돌아올 수 없음
Note over U,W: 서로를 기다림 ── 데드락
그림 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 워커 스레드 자체를 기다리는 일은 조건 변수 기사에서 다룬 규율로 쓰십시오.
flowchart TB
accTitle: 멈추지 않는 앱의 역할 분담
accDescr: UI 스레드는 입력 수락, 진행 표시, 취소 수락만 담당하고, 워커 스레드가 무거운 작업을 돌린 뒤 PostMessage 또는 await 연속으로 완료를 UI 스레드에 되돌린다
ui["UI 스레드: 입력, 진행, 취소"] -->|"작업을 넘긴다"| w["워커 스레드: 무거운 작업"]
w -->|"PostMessage / await 연속"| ui
ui -.-> ng["UI 스레드에서 동기 I/O나 긴 계산을 하지 않는다"]
그림 6: UI 스레드는 「접수 창구」로 두고, 무거운 작업은 항상 워커에 넘기며, 완료 알림만 받는다.
피하고 싶은 것은 표시를 살리려고 무거운 작업 덩어리 사이에 Application.DoEvents()나 PeekMessage 루프를 끼워 넣는 기법입니다. 응답 없음은 피하지만, 임의의 이벤트 핸들러가 작업 한가운데에 재진입합니다. 버튼의 두 번째 클릭, 처리 중의 폼 닫기, 타이머 발화── 어느 것이든 아직 처리 중인 데이터를 망가뜨릴 수 있고, 버그는 타이밍 의존이며 재현이 어렵습니다. 메시지 루프의 수동 펌핑은 모달 진행 대화상자 같은 한정된 구조 안에 두고, 원칙적으로는 분리로 해결하십시오.
sequenceDiagram
accTitle: DoEvents가 일으키는 재진입 버그의 타임라인
accDescr: 무거운 작업 한가운데에서 DoEvents를 호출하면 큐에 있던 클릭의 이벤트 핸들러가 끼어들어 아직 처리 중인 데이터를 다시 쓴 뒤 원래 작업이 재개되어, 타이밍 의존의 데이터 파손이 난다
participant U as UI 스레드
U->>U: 무거운 작업 시작(처리 중인 데이터)
U->>U: DoEvents(큐의 메시지를 처리)
Note over U: 버튼 재클릭 핸들러가 끼어든다
U->>U: 끼어든 작업이 데이터를 다시 쓴다
U->>U: 원래 작업이 재개된다(데이터는 이미 불일치)
그림 7: DoEvents는 「응답 없음」을 지우는 대가로, 작업 한가운데에 임의의 이벤트를 불러들인다.
오래 도는 작업에는 진행 표시와 취소도 설계에 넣습니다. 진행은 IProgress<T>로 UI에 보내고, 중단은 CancellationToken으로 전달하면, 사용자는 「돌아가고 있다」는 것을 보고 강제 종료에 손을 대지 않습니다(강제 종료는 종종 데이터 파손의 원인입니다).
flowchart TB
accTitle: 오래 도는 작업의 진행과 취소 흐름
accDescr: 워커 스레드는 IProgress로 진행을 UI 스레드에 보내고, UI의 취소 동작은 CancellationToken으로 워커에 닿으며, 워커는 편리한 경계에서 멈추고 정리한다
w3["워커: 오래 도는 작업"] -->|"IProgress로 진행"| ui2["UI: 진행과 중지 버튼"]
ui2 -->|"CancellationToken"| w3
w3 --> stop2["경계에서 멈추고 정리한다"]
그림 8: 진행은 「워커 → UI」, 취소는 「UI → 워커」이다. 이 얇은 양방향 채널을 처음부터 설계에 넣는다.
6. 멈춘 순간의 조사
「가끔 멈춘다」는 조사에서 가장 가치 있는 것은 바로 그 멈춘 순간의 스레드 상태입니다. 재부팅하면 증거는 사라집니다.
덤프를 뜹니다. 작업 관리자의 세부 정보 탭에서 대상 프로세스를 오른쪽 클릭 → 「덤프 파일 만들기」. 그것만으로 모든 스레드의 스택이 들어 있는 전체 덤프를 얻습니다. 티켓을 받는 IT 담당자에게 「멈췄을 때, 닫기 전에 이것을 떠 달라」고 말하는 것만으로 조사 성공률이 크게 바뀝니다. 수집 메커니즘을 만드는 일은 크래시 덤프 수집 기사를 보십시오.
UI 스레드의 스택을 봅니다. 덤프를 WinDbg로 열고 메시지 루프를 펌핑하는 스레드(보통 스레드 0)의 스택을 봅니다. 동기 I/O는 ReadFile이나 네트워크 API로, 락 대기는 WaitFor… 계열 호출로, 스레드를 넘는 SendMessage는 SendMessage 안의 대기로── 그대로 나옵니다. 읽는 방법은 WinDbg 입문 기사에서 설명합니다.
살아 있는 채로 봅니다. Process Explorer로는 스레드 목록과 스택을 그 자리에서 검사할 수 있습니다. 꾸준히 버벅일 때는 WPR 트레이스를 떠서 UI 스레드의 대기를 시간에 따라 분석합니다(WPR/WPA in Practice).
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
그림 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초 동안 돌아오지 않았다」는 정확한 문장으로 번역할 수 있습니다. 그 한 문장에서 거꾸로 가면, 후보 원인, 수정, 조사 절차가 모두 자연스럽게 나옵니다.
관련 기사
- 스퓨리어스 웨이크업 ── 조건 변수가 「알림 없이」깨어나는 이유와 Windows에서 올바르게 대기하는 방법
- 멀티스레드 실무 베스트 프랙티스 .NET 편 ── 스레드를 늘리기 전에 정해둘 것
- COM STA/MTA 기초 - 스레드 모델과 행(hang)을 피하는 사고방식
- WinDbg + SOS로 크래시 덤프 읽기 ── 수집 이후 실무 분석 입문
- Process Explorer / Handle / VMMap 실전 ── 행(hang)・누수・「파일 사용 중」을 지금 이 순간의 상태에서 추적한다
- Windows Shutdown as Seen from Your App — Surviving Exit Notifications, Restarts, and Power Loss Correctly
관련 상담 영역
합동회사 코무라소프트에서는 「가끔 멈춘다」, 「응답 없음」이 되는 업무 앱의 원인 조사(덤프 분석과 트레이스 분석), 동기 작업으로 가득한 레거시 UI 코드를 async/await와 워커 스레드 분리로 리팩터하는 작업, 멈추지 않는 UI 설계의 리뷰를 다루고 있습니다. 아직 재현 절차가 없어도, 증거를 모으는 설계부터 도울 수 있습니다.
참고 링크
-
Microsoft Learn, IsHungAppWindow function (winuser.h). 앱이 「입력을 기다리지 않고, 기동 시퀀스에 있지 않으며, 내부 타임아웃 5초 동안 PeekMessage를 호출하지 않았을 때」 응답하지 않는다고 보는 판단 기준, 이 5초 기준이 바뀔 수 있다는 점, 고스트 창에 대해서는 함수가 항상 TRUE를 반환한다는 점에 대해. ↩ ↩2
-
Microsoft Learn, GetMessage function (winuser.h). 최상위 창이 수 초 동안 메시지에 응답하지 않으면 시스템이 응답하지 않는다고 보고 같은 Z 순서·위치·크기·외관의 고스트 창으로 바꾼다는 점, 사용자는 이동·크기 변경·닫기만 할 수 있다는 점, 디버거가 붙어 있는 동안에는 고스트 창이 만들어지지 않는다는 점에 대해. ↩ ↩2 ↩3
-
Microsoft Learn, About Messages and Message Queues. Windows 앱이 이벤트 구동이며 윈도우 프로시저가 메시지를 처리한다는 점, 큐 메시지와 직접 보낸 메시지의 구분, 응답하지 않는 창을 고스트 창으로 바꾸는 점, 스레드가 서로 메시지를 보내 데드락이 나는 절에 대해. ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, How to make thread-safe calls to controls (Windows Forms). WinForms 컨트롤이 만든 스레드 이외에서 건드리면 안전하지 않다는 점, 다른 스레드에서의 갱신에 Invoke/BeginInvoke를 쓴다는 점, async/await 또는 BackgroundWorker를 쓰는 안전한 비동기 패턴에 대해. ↩ ↩2
-
Microsoft Learn, Using Messages and Message Queues. GetMessage, TranslateMessage, DispatchMessage를 쓰는 전형적인 메시지 루프 구현과 메시지 큐를 검사하는 방법에 대해. ↩
-
Microsoft Learn, SendMessage function (winuser.h). SendMessage가 지정한 창의 윈도우 프로시저를 호출하고 처리가 끝날 때까지 돌아오지 않는다는 점, 다른 스레드의 창으로 보내면 그 스레드가 메시지를 처리할 때까지 송신 측이 기다리게 된다는 점, 응답을 기다리지 않고 큐에 놓는 PostMessage와의 차이에 대해. ↩ ↩2 ↩3
-
Microsoft Learn, DisableProcessWindowsGhosting function (winuser.h). 호출하는 GUI 프로세스에서, 응답하지 않는 창을 최소화·이동·닫을 수 있게 하는 고스트 창 기능을 끌 수 있다는 점, 해제가 프로세스 수명 동안 유지된다는 점에 대해. ↩
-
Microsoft Learn, SendMessageTimeout function (winuser.h). 타임아웃을 두고 메시지를 보낼 수 있다는 점, 창이 응답하지 않을 때(행으로 판단되었을 때) 기다리지 않고 돌아오는 플래그(SMTO_ABORTIFHUNG)에 대해. ↩
관련 기사
같은 태그를 공유하는 최신 기사입니다. 더 가까운 주제로 지식을 넓힐 수 있습니다.
DllMain과 로더 락 ── 「DLL 초기화에서는 아무것도 하지 말라」는 말의 진짜 이유
DllMain에서 LoadLibrary를 호출하거나 다른 스레드와 동기화해서는 안 되는 이유를 설명합니다. 로더 락이 모든 DLL 알림을 직렬화하는 구조, 데드락이 구조적으로 확정되는 고전적 시나리오, 초기화를 미루는 올바른 설계, 행(hang)...
Windows 앱 외주·수탁 개발을 의뢰하기 전에 정리해야 할 것
Windows 앱의 외주·수탁 개발을 의뢰하기 전에, 기존 소프트웨어 개수, 장치 연계, COM/ActiveX, 배포·업데이트, 보수와 관련해 정리해야 할 포인트를 설명합니다.
Win32 스레드 풀 API ── CreateThreadpoolWork로 「스레드를 만들지 않는」병렬 처리
네이티브 코드에서 CreateThread를 여기저기 만들고 있지는 않은가요. Vista에서 개편된 Win32 스레드 풀 API의 work·timer·wait·io 네 객체, 클린업 그룹, 콜백에서 해서는 안 되는 일까지 1차 정보를 바탕으로 해설...
절전에서 재개하면 깨지는 앱 ── Windows 전원 이벤트의 구조와 살아남는 업무 앱
노트북을 열었더니 업무 앱의 연결이 죽어 있었다 ── 원인은 절전을 전제하지 않은 설계입니다. WM_POWERBROADCAST 알림의 흐름, Modern Standby의 동작, 끊김/재연결 설계, 절전 억제, 조사 명령을 1차 정보로 정리합니다.
스퓨리어스 웨이크업 ── 조건 변수가 「알림 없이」깨어나는 이유와 Windows에서 올바르게 대기하는 방법
조건 변수의 대기는 알림이 오지 않아도 돌아올 수 있습니다(스퓨리어스 웨이크업). Windows 구현에서 사양이 그것을 허용하는 이유를 설명하고, while 루프와 술어로 올바르게 대기하는 방법을 Win32, C++, C#으로 보입니다.
관련 토픽
이 기사와 가까운 토픽 페이지입니다. 기사를 출발점 삼아 관련 서비스와 다른 기사로 이어집니다.
Windows 기술 토픽
Windows 개발, 장애 조사, 기존 자산 활용에 관한 KomuraSoft LLC 기사를 모은 토픽 허브입니다.
UI 스레드 & 타이머
WPF / WinForms UI 스레드, async 흐름, Dispatcher 사용, 타이머 판단을 정리한 토픽 페이지입니다.
이 주제와 연결되는 서비스
이 기사는 다음 서비스 페이지로 이어집니다. 가까운 입구부터 확인해 주세요.
Windows 앱 개발
상주 처리, 장비 연동, 운영 로그, 유지 보수 가능한 구조가 필요한 Windows 데스크톱 애플리케이션을 지원합니다.
자주 묻는 질문
이 기사 주제에 대해 상담 시 자주 나오는 질문을 모았습니다.
- 「응답 없음」은 어떤 조건에서 나타나나요?
- 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 실전도 보십시오.