본문 바로가기
카테고리 없음

VS2022 WinForms: 종료 안 되는 스레드 빠르게 찾는 법

by 공부봇 2025. 8. 29.
반응형

VS2022 WinForms: 종료 안 되는 스레드 빠르게 찾는 법


1) 지금 걸린 바로 그 스레드를 찾기 (가장 빠름)

  1. 멈추기(Break All)
    디버깅 중 종료가 안 될 때 Debug ▶ Break All(단축키 Ctrl+Alt+Break).
  2. 이 순간 프로세스의 모든 스레드가 정지하고, 현재 각 스레드가 “어디서 무엇을 하고 있는지” 스냅샷을 볼 수 있습니다.
  3. Threads 창 열기
    Debug ▶ Windows ▶ Threads.
    • 열의 의미
      • ID: OS 스레드 ID
      • Category: UI/Worker/ThreadPool 구분
      • Location: 현재 스택의 최상단 프레임
      • Name: 코드에서 설정한 스레드명(있다면)
    • 의심 신호
      • Location에 WaitSleepJoin, Monitor.Enter, Semaphore.Wait, WaitOne, Join 등이 보이면 “대기/교착/종료대기” 가능성 큼.
      • 같은 스레드가 항상 동일한 위치에서 멈춘다면 루프/대기 고착 패턴.
  4. Call Stack/Source로 파고들기
    Threads 창에서 의심 스레드 더블클릭 → Call Stack을 확인.
    • Show External Code 켜서(.NET 내부까지) 스택을 넓게 봅니다.
    • 스택 상단에서 내 코드 프레임(粗체)까지 내려가면, 어디서 기다리고 있는지 바로 확인됩니다.
    • 양쪽 스레드가 서로 Join/lock/WaitHandle로 맞물려 있으면 교착 후보.

팁: Tools ▶ Options ▶ Debugging ▶ Symbols에서 Microsoft Symbol Servers 체크해 두면 프레임 이름이 훨씬 읽기 좋아집니다.


2) 시각화로 한 번에 구조 파악 (병행/비동기 코드에 특히 유용)

  1. Parallel Stacks 창
    Debug ▶ Windows ▶ Parallel Stacks
    • 스레드들의 호출 스택이 상자-화살표 그래프로 나타납니다.
    • 상자가 서로 가리키며 멈춰 있으면 교착이나 대기 체인을 한눈에 볼 수 있어 좁히기 쉽습니다.
  2. Parallel Tasks 창 (async/Task 기반일 때)
    Debug ▶ Windows ▶ Parallel Tasks
    • Waiting, Scheduled, Running 등 상태로 Task들을 보여줍니다.
    • 특정 Task가 완료되지 못한 이유(Await 대기 지점)와 연결된 스레드를 추적할 수 있습니다.
  3. Concurrency Visualizer (확장)
    • VS2022용 확장을 설치하면 시간축 상에서 스레드 상태(실행/대기/블로킹)를 분석 가능합니다.
    • “어떤 락/핸들이 오래 잡고 있었는지”를 시간 기준으로 추적하기 좋습니다.

3) 종료 후에도 프로세스가 남을 때(또는 재현 어려울 때) – 덤프/외부도구

  1. 덤프 뜨기
    • VS에서 Debug ▶ Save Dump As… (with heap) 또는
    • 작업관리자/ProcExp에서 메모리 덤프 저장.
  2. 덤프 열어 스택 확인
    • VS로 File ▶ Open ▶ File… 에서 덤프 열기 → Threads/Call Stack 확인.
    • 더 깊게: WinDbg + SOS
      • !threads, ~* kb(모든 스레드 스택), !clrstack(관리 스택), !syncblk(모니터 잠금), !dlk(교착감지) 등으로 정확히 어디에/무엇 때문에 멈췄는지 파악 가능.
  3. 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) 한눈에 보는 “문제 재현 시 체크리스트”

  1. Break AllThreads 창 열고 Location/Call Stack 확인
  2. Parallel Stacks/Tasks로 대기 체인/미완료 Task 확인
  3. 필요 시 덤프 저장 후 VS/WinDbg로 정밀 분석
  4. 코드 측면에서
    • 워커는 IsBackground = true
    • 토큰 기반 종료 + Join/Wait 타임아웃/로그
    • 락 순서 통일, TryEnter로 교착 진단
    • async Task/await 누락 제거

원하시면 **현재 솔루션의 특정 스레드(예: AlarmDisplayScheduler, Grablink, IlluminationThread 등)**를 기준으로, FormClosing 종료 루틴과 진단 로그 포인트를 바로 넣을 수 있게 템플릿 코드를 만들어 드릴게요.

반응형