반응형
VS2022 WinForms: 종료 안 되는 스레드 빠르게 찾는 법
1) 지금 걸린 바로 그 스레드를 찾기 (가장 빠름)
- 멈추기(Break All)
디버깅 중 종료가 안 될 때 Debug ▶ Break All(단축키 Ctrl+Alt+Break). - 이 순간 프로세스의 모든 스레드가 정지하고, 현재 각 스레드가 “어디서 무엇을 하고 있는지” 스냅샷을 볼 수 있습니다.
- Threads 창 열기
Debug ▶ Windows ▶ Threads.- 열의 의미
- ID: OS 스레드 ID
- Category: UI/Worker/ThreadPool 구분
- Location: 현재 스택의 최상단 프레임
- Name: 코드에서 설정한 스레드명(있다면)
- 의심 신호
- Location에 WaitSleepJoin, Monitor.Enter, Semaphore.Wait, WaitOne, Join 등이 보이면 “대기/교착/종료대기” 가능성 큼.
- 같은 스레드가 항상 동일한 위치에서 멈춘다면 루프/대기 고착 패턴.
- 열의 의미
- Call Stack/Source로 파고들기
Threads 창에서 의심 스레드 더블클릭 → Call Stack을 확인.- Show External Code 켜서(.NET 내부까지) 스택을 넓게 봅니다.
- 스택 상단에서 내 코드 프레임(粗체)까지 내려가면, 어디서 기다리고 있는지 바로 확인됩니다.
- 양쪽 스레드가 서로 Join/lock/WaitHandle로 맞물려 있으면 교착 후보.
팁: Tools ▶ Options ▶ Debugging ▶ Symbols에서 Microsoft Symbol Servers 체크해 두면 프레임 이름이 훨씬 읽기 좋아집니다.
2) 시각화로 한 번에 구조 파악 (병행/비동기 코드에 특히 유용)
- Parallel Stacks 창
Debug ▶ Windows ▶ Parallel Stacks- 스레드들의 호출 스택이 상자-화살표 그래프로 나타납니다.
- 상자가 서로 가리키며 멈춰 있으면 교착이나 대기 체인을 한눈에 볼 수 있어 좁히기 쉽습니다.
- Parallel Tasks 창 (async/Task 기반일 때)
Debug ▶ Windows ▶ Parallel Tasks- Waiting, Scheduled, Running 등 상태로 Task들을 보여줍니다.
- 특정 Task가 완료되지 못한 이유(Await 대기 지점)와 연결된 스레드를 추적할 수 있습니다.
- Concurrency Visualizer (확장)
- VS2022용 확장을 설치하면 시간축 상에서 스레드 상태(실행/대기/블로킹)를 분석 가능합니다.
- “어떤 락/핸들이 오래 잡고 있었는지”를 시간 기준으로 추적하기 좋습니다.
3) 종료 후에도 프로세스가 남을 때(또는 재현 어려울 때) – 덤프/외부도구
- 덤프 뜨기
- VS에서 Debug ▶ Save Dump As… (with heap) 또는
- 작업관리자/ProcExp에서 메모리 덤프 저장.
- 덤프 열어 스택 확인
- VS로 File ▶ Open ▶ File… 에서 덤프 열기 → Threads/Call Stack 확인.
- 더 깊게: WinDbg + SOS
- !threads, ~* kb(모든 스레드 스택), !clrstack(관리 스택), !syncblk(모니터 잠금), !dlk(교착감지) 등으로 정확히 어디에/무엇 때문에 멈췄는지 파악 가능.
- Process Explorer/Process Hacker
- 실행 중인 프로세스 선택 → Threads 탭 → 각 스레드 Stack 보기.
- 네이티브 대기(WaitForSingleObject, SleepConditionVariableCS 등)도 포착되어 블로킹 지점 추정에 좋습니다.
4) “왜 안 꺼지나요?”에서 자주 보는 패턴과 해법
- Foreground 스레드가 루프/대기에 걸림
- 해법: 워커는 IsBackground = true 사용(프로세스 종료를 막지 않음) + 협조적 종료(토큰/이벤트로 루프 탈출).
- Join/WaitOne 무기한 대기
- 해법: Join(timeout), WaitOne(timeout)과 타임아웃 로그를 두어 어디서 막혔는지 기록.
- lock 교착(스레드 A가 자원1, B가 자원2를 잡고 서로 반대 자원 대기)
- 해법: 락 순서 통일, 락 범위 축소, Monitor.TryEnter(timeout) 등으로 진단/완화.
- async/await 누락 (fire-and-forget, async void, 컨텍스트 교착)
- 해법: 가능하면 async Task 서명, await 누락 제거, 컨텍스트 필요 없으면 .ConfigureAwait(false).
- FormClosing에서 장시간 Join/Dispose
- 해법: FormClosing에서 취소 토큰 신호 → 비동기 종료 → 일정 시간 내 종료 실패 시 강제 포기(로그 남김). UI 스레드 블로킹 금지.
5) 찾기 쉽게 만드는 사전 작업(강력 추천)
- 스레드 이름 붙이기Threads 창에서 바로 식별 가능.
- var t = new Thread(Work){ IsBackground = true, Name = "IlluminationThread" }; t.Start();
- 협조적 종료 패턴(토큰/이벤트)
- class Worker : IDisposable { private readonly CancellationTokenSource _cts = new(); private Thread _thread; public void Start() { _thread = new Thread(() => Run(_cts.Token)) { IsBackground = true, Name = "GrabWorker" }; _thread.Start(); } private void Run(CancellationToken ct) { while (!ct.IsCancellationRequested) { // 작업... // 대기는 WaitHandle.WaitOne(timeout) 또는 Task.Delay(timeout, ct) 등으로 } } public void Stop(int timeoutMs = 2000) { _cts.Cancel(); if (_thread != null && !_thread.Join(timeoutMs)) { // 타임아웃: 로그 남기고 포기(프로세스 종료는 막지 않음) Debug.WriteLine("GrabWorker stop timeout"); } } public void Dispose() { Stop(); _cts.Dispose(); } }
- 중요 지점 로깅
- 스레드 시작/종료, 대기 진입/이탈, 타임아웃 발생을 ID/Name 포함해서 로그 → 현장 재현 없이도 원인 역추적이 쉬워집니다.
6) 한눈에 보는 “문제 재현 시 체크리스트”
- Break All → Threads 창 열고 Location/Call Stack 확인
- Parallel Stacks/Tasks로 대기 체인/미완료 Task 확인
- 필요 시 덤프 저장 후 VS/WinDbg로 정밀 분석
- 코드 측면에서
- 워커는 IsBackground = true
- 토큰 기반 종료 + Join/Wait 타임아웃/로그
- 락 순서 통일, TryEnter로 교착 진단
- async Task/await 누락 제거
원하시면 **현재 솔루션의 특정 스레드(예: AlarmDisplayScheduler, Grablink, IlluminationThread 등)**를 기준으로, FormClosing 종료 루틴과 진단 로그 포인트를 바로 넣을 수 있게 템플릿 코드를 만들어 드릴게요.
반응형