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. 단 남발하면 풀 경합.