← 문제로

8. 질문 — async/await의 동작 원리와 함정 (C#)

난이도 상
내 답안
모범답안

모범답안 — async/await 동작 원리와 함정 (C#)

난이도: 상

핵심 답변

async/await는 컴파일러가 메서드를 상태머신으로 변환해, await 지점에서 메서드를 중단하고 스레드를 반납한 뒤, 기다리던 작업이 끝나면 이어서(continuation) 실행한다. 즉 "기다리는 동안 스레드를 붙잡지 않는" 것이 핵심이다. 여기에 .Result/.Wait()로 동기 대기를 걸면 스레드를 붙잡은 채 기다리게 되어 (1) 동기화 컨텍스트가 있는 환경에선 데드락, 서버에선 (2) 스레드풀 고갈로 이어진다.

깊이 있는 설명

동작 원리

  • 컴파일러가 async 메서드를 상태머신 구조체로 변환. await task는 "task가 이미 끝났으면 계속, 아니면 continuation을 등록하고 return"으로 컴파일된다.
  • 블로킹(Thread.Sleep, .Result)은 스레드가 깨어날 때까지 점유. 비동기 대기(await)는 스레드를 풀에 돌려주므로, 같은 스레드로 다른 요청을 처리할 수 있다 → 적은 스레드로 많은 동시 IO 처리.

.Result/.Wait()가 위험한 이유

  • 데드락: UI/ASP.NET 클래식처럼 단일 동기화 컨텍스트가 있으면, await 이후 continuation이 "원래 컨텍스트"로 돌아가려 한다. 그런데 그 컨텍스트 스레드는 .Result로 블로킹 중 → 서로를 기다리는 교착.
  • 스레드풀 고갈: 콘솔/서버는 보통 컨텍스트가 없어 데드락은 안 나도, 비동기 작업을 동기로 기다리느라 풀 스레드를 점유. 동시 요청이 많으면 풀이 바닥나 새 작업이 큐에 쌓이고 사실상 멈춘다.

ConfigureAwait(false)

  • "continuation을 원래 동기화 컨텍스트로 되돌리지 말고 아무 풀 스레드에서 이어가라"는 지시. 라이브러리/서버 코드는 특정 컨텍스트가 필요 없으므로 ConfigureAwait(false)로 컨텍스트 캡처 비용과 데드락 위험을 줄인다.

응용/실무 연결

  • 게임서버 규칙: IO는 async로, 단 락을 잡은 채 await 하지 말 것. .Result/.Wait() 금지.
  • 룸 로직 같은 핫 루프는 보통 단일 스레드 잡 큐로 직렬 처리하고, DB 같은 느린 IO만 async로 빼서 결과를 잡 큐에 다시 넣는 패턴이 흔하다.

흔한 오답·함정

  • "async를 붙이면 무조건 빨라진다" — 아니다. CPU 바운드 작업엔 오히려 상태머신·할당 오버헤드만 늘어난다. async는 IO 대기 동안 스레드를 아끼는 것이지 연산을 빠르게 하는 게 아니다.
  • 핫 루프에서 매 틱 await → Task 객체 할당이 누적돼 GC 압박. ValueTask나 동기 경로로 최적화.
  • async void 사용(이벤트 핸들러 외) → 예외를 잡을 수 없고 추적 불가.

꼬리질문 대비

  • Q. Task와 ValueTask 차이? Task는 참조 타입(힙 할당). ValueTask는 동기 완료가 잦은 핫패스에서 할당을 피하기 위한 구조체 기반. 단 두 번 await 등 오용 주의.
  • Q. await가 항상 다른 스레드에서 재개되나? 아니다. 컨텍스트/스케줄러에 따라 같은 스레드일 수도 있다. 동기 완료면 스레드 전환 없이 그대로 진행.
  • Q. CPU 바운드 작업을 비동기로 처리하려면? Task.Run으로 풀 스레드에 offload. 단 남발하면 풀 경합.