1. async/await 의 내부 동작(상태 머신)과 동기 블로킹 데드락
난이도 상내 리뷰 · C#
해설 · C#
해설 — async/await 의 내부 동작(상태 머신)과 동기 블로킹 데드락
난이도: 상
답변 프레임워크: 요약 → 핵심 개념 → 단계별 내부 동작 → 흔한 오해·함정 → 면접 포인트
요약
async/await 는 컴파일러가 상태 머신(state machine) 으로 변환한다. await 지점에서
메서드는 일시 중단되고, 기다리던 작업이 끝나면 continuation(이어지는 코드) 이 다시
스케줄된다. 이때 기본적으로 캡처된 SynchronizationContext(또는 TaskScheduler) 위에서
이어지려 한다. Deadlock() 은 SynchronizationContext 가 있는 환경(UI 스레드, 구 ASP.NET
요청 컨텍스트)에서 그 컨텍스트 스레드를 .Result 로 블록한다. LoadAsync 의 await Task.Delay 이후 continuation 은 그 같은(블록된) 컨텍스트 로 돌아오려 하지만, 컨텍스트는
이미 .Result 로 점유돼 있어 서로를 기다리는 데드락이 된다. 게임서버 워커(컨텍스트 없는
스레드풀)에선 데드락은 잘 안 나지만, 스레드풀 고갈·응답성 저하의 원인이 된다.
핵심 개념
- await ≠ 블로킹:
await는 스레드를 멈추는 게 아니라, 완료 시 실행할 continuation 을 등록하고 호출 스택을 반환한다(메서드는Task를 즉시 돌려줌). - 컨텍스트 캡처:
await시점에SynchronizationContext.Current(없으면 현재TaskScheduler)를 캡처해, continuation 을 거기로 다시 게시(post)한다. .Result/.Wait():Task가 끝날 때까지 현재 스레드를 동기 블록. 컨텍스트 스레드를 블록하면 그 컨텍스트로 돌아와야 하는 continuation 이 영원히 못 돌아온다.
단계별 내부 동작
- 컴파일러는
LoadAsync를IAsyncStateMachine을 구현하는 구조체로 변환한다. 본문은MoveNext()안의 상태별 switch 로 쪼개진다(상태 -1/0/1 …). - 첫
MoveNext:Task.Delay(100)의 awaiter 를 얻고IsCompleted를 검사. 아직 미완료이므로awaiter.OnCompleted(MoveNext)로 continuation 등록 후 반환(메서드는 미완료Task를 호출자에게 돌려줌). 이때AsyncTaskMethodBuilder가 컨텍스트를 캡처. - 100ms 후
Delay완료 → 등록된 continuation 이 캡처된 컨텍스트에 게시되어MoveNext가 다시 호출됨 →return 42로 결과Task를 완료. Deadlock(): 2번에서 반환된 미완료Task에.Result로 블록. 컨텍스트 스레드가 블록됨 → 3번의 continuation 이 그 컨텍스트로 post 되지만 처리될 수 없음 → 상호 대기(데드락).
흔한 오해·함정
- "async 면 새 스레드에서 돈다" ✗ —
await전까지는 호출 스레드에서 동기 실행되고,Task.Delay같은 I/O 성 대기는 스레드를 점유하지 않는다(타이머/완료포트). 새 스레드가 생기는 게 아니다. .Result/.Wait()로 동기 대기 ✗ — 컨텍스트 있는 환경에선 데드락, 없는 환경에서도 스레드풀 스레드를 블록해 고갈을 유발. 해법: 끝까지 async(호출 체인을await로 전파).ConfigureAwait(false): continuation 을 캡처된 컨텍스트로 되돌리지 않고 스레드풀에서 잇는다 →LoadAsync내부를await Task.Delay(100).ConfigureAwait(false)로 바꾸면 이 데드락은 사라진다(라이브러리 코드 권장). 단 근본 해법은 동기 블로킹을 없애는 것.async void(C) ✗ — 반환Task가 없어 호출자가 완료/예외를 관측 못 함.throw는 잡히지 않고SynchronizationContext로 올라가 프로세스 크래시가 될 수 있다. 이벤트 핸들러 외에는async Task를 쓴다.
수정 방향(설명용)
// 끝까지 async: 동기 블로킹 제거
public async Task<int> NoDeadlockAsync() => await LoadAsync();
// 라이브러리 내부라면 컨텍스트 복귀를 끊는다
public async Task<int> LoadAsync()
{
await Task.Delay(100).ConfigureAwait(false);
return 42;
}
면접 포인트 (예상 질문)
await한 줄이 컴파일 후 무엇이 되나(상태 머신/MoveNext/awaiter.OnCompleted)?await가 스레드를 블록하지 않는다는 게 무슨 뜻인가?.Result데드락이 일어나는 메커니즘을 컨텍스트 캡처 관점에서 단계별로 설명하라.ConfigureAwait(false)가 왜 이를 푸는가, 그리고 왜 그게 근본 해법은 아닌가?async void와async Task의 차이(예외 전파·관측 가능성)는? 언제async void가 정당한가?