19. Task 조합의 예외·취소 의미론(WhenAll/await rethrow/CancellationToken/미관측 예외)
난이도 상내 리뷰 · C#
해설 · C#
해설 — Task 조합의 예외·취소 의미론(WhenAll/await rethrow/CancellationToken/미관측 예외)
난이도: 상
요약
await 로 faulted Task 를 기다리면 AggregateException 이 아니라 그 안의 "첫 예외" 하나만 다시 던져진다. Task.WhenAll 은 모든 예외를 Task.Exception(AggregateException)에 집계하되, await 지점에선 첫 것만 rethrow. 취소는 토큰이 일치하는 OperationCanceledException 이면 Task 가 Faulted 가 아니라 Canceled 로 끝난다. WhenAny 의 진 쪽 예외와 async void 예외는 미관측되기 쉬워 위험하다.
예측 출력
caught InvalidOperationException: A
all.IsFaulted=True inner=2
work.IsCanceled=True
핵심 개념 · 단계별
줄1~2: WhenAll 의 예외 집계 vs await 의 rethrow
t1("A", 10ms),t2("B", 30ms) 둘 다InvalidOperationException을 던진다.Task.WhenAll(t1,t2)의 결과 Task 는 두 예외를 모두 담은AggregateException을all.Exception에 갖는다(순서는 입력 Task 순서 = A, B).- 그러나
await all;은 편의상 AggregateException 의 첫 InnerException 하나만 다시 던진다 →catch (Exception ex)는InvalidOperationException, 메시지"A". → 줄1 =caught InvalidOperationException: A. - Task 객체는 여전히 faulted 이고 두 예외를 보존 →
all.IsFaulted=True,all.Exception.InnerExceptions.Count=2. → 줄2. - 둘 다 처리하려면:
await로 잡지 말고all.Exception을 직접 순회하거나,try { await all; } catch { foreach (var e in all.Exception.InnerExceptions) ... }로 집계 예외를 열어본다.
줄3: 협조적 취소 → Canceled 상태
Task.Run(..., cts.Token)에 토큰을 넘겼고, 본문이cts.Token.ThrowIfCancellationRequested()로 같은 토큰의OperationCanceledException을 던진다.- TPL 규칙: 던져진 OCE 의 토큰이 Task 에 연결된 토큰과 일치하면 그 Task 는 Canceled(예외적 종료가 아님)로 전이. →
work.IsCanceled=True. → 줄3. - 만약 토큰이 불일치하거나 아무 토큰도 안 넘겼다면 같은 OCE 라도 Faulted 로 처리됐을 것(이 구분이 자주 나오는 함정).
await work는 취소 시TaskCanceledException(OCE 파생)을 던지므로catch (OperationCanceledException)로 잡힌다.
흔한 오해·함정
await는 첫 예외만 던진다: 병렬 작업의 "모든 실패"를 봐야 하는데await만 하면 나머지를 놓친다.Task.Exception(AggregateException) 을 봐야 한다.- WhenAny 의 미관측 예외:
await Task.WhenAny(a, b)는 먼저 끝난 하나만 돌려준다. 진 쪽이 나중에 예외로 끝나면 아무도 그 Task 를 await/관측하지 않아 UnobservedTaskException 이 된다(예전엔 파이널라이저에서 프로세스 종료까지). → 진 쪽도 나중에 반드시await(또는.Exception관측)해야 한다. async void: 반환 Task 가 없어 호출자가await/try-catch로 예외를 잡을 수 없다. 예외는 캡처된SynchronizationContext(또는 스레드풀)로 올라가 앱을 죽인다. 이벤트 핸들러 외엔async void금지 —async Task를 쓴다(csharp_internals/problem1상태머신과 연결).- 취소는 협조적: 토큰을 넘겨도 본문이
ThrowIfCancellationRequested/토큰 인지 API 를 호출해야 실제로 멈춘다.Task.Delay(ms)(토큰 없이)나 CPU 루프는 취소를 인지하지 못한다 →Task.Delay(ms, token)처럼 토큰 전파 필요. - fire-and-forget 금지:
_ = SomethingAsync();는 예외를 삼킨다(session_network/problem29아웃바운드 연결의 미관측 예외와 같은 뿌리).
면접 포인트
- WhenAll 의 예외 집계와
awaitrethrow 규칙(첫 예외 하나) — 모든 실패를 보려면? - 토큰 일치 OCE 가 Faulted 가 아니라 Canceled 가 되는 조건, 협조적 취소의 의미.
- WhenAny 미관측 예외·async void 예외 전파 — 왜 위험하고 어떻게 피하는가.