빠른 흐름
// task를 먼저 시작해두고 한 번에 기다림
Task<User> userTask = LoadUserAsync();
Task<Order[]> orderTask = LoadOrdersAsync();
await Task.WhenAll(userTask, orderTask); // 두 작업이 동시에 진행됨
User user = await userTask; // 이미 완료됨 — 바로 반환
Order[] orders = await orderTask;
// 컬렉션 버전
string[] results = await Task.WhenAll(
urls.Select(url => http.GetStringAsync(url)));기본 흐름
어떤 병렬 대기 형태가 있나
| 상황 | 먼저 떠올릴 것 |
|---|---|
| 여러 작업이 모두 끝나야 함 | Task.WhenAll |
| 가장 먼저 끝난 것만 필요함 | Task.WhenAny |
| 동시에 너무 많이 돌리면 안 됨 | SemaphoreSlim + WhenAll |
순차 await vs WhenAll — 핵심 차이
await A(); await B(); 는 A가 완료된 후에 B가 시작됩니다. WhenAll 은 두 작업을 동시에 시작하고 모두 완료될 때까지 기다립니다.
// ❌ 순차 — A(2초) + B(1초) = 3초
var user = await LoadUserAsync(); // 2초 기다린 후
var orders = await LoadOrdersAsync(); // 1초 기다림 → 총 3초
// ✅ 동시 — max(A, B) = 2초
var userTask = LoadUserAsync(); // 즉시 시작
var orderTask = LoadOrdersAsync(); // 즉시 시작
await Task.WhenAll(userTask, orderTask); // 가장 오래 걸리는 것 = 2초핵심은 WhenAll 자체가 아니라 "task를 먼저 만들어 두는 것" 입니다. task를 생성하는 순간 비동기 작업이 시작됩니다.
// ❌ Task.WhenAll 없이 순차 await만 하면 총 시간이 누적
var user = await LoadUserAsync();
var orders = await LoadOrdersAsync();
// ✅ task를 먼저 시작한 뒤 한 번에 기다림
var userTask = LoadUserAsync();
var orderTask = LoadOrdersAsync();
await Task.WhenAll(userTask, orderTask);예외 처리 — AggregateException
여러 task 중 하나가 실패하면 AggregateException 에 모든 예외가 묶여서 던져집니다. await하면 첫 번째 예외만 언래핑되므로, 모든 예외를 확인하려면 추가 처리가 필요합니다.
var t1 = Task.FromException(new InvalidOperationException("A 실패"));
var t2 = Task.FromException(new ArgumentException("B 실패"));
try
{
await Task.WhenAll(t1, t2);
}
catch (Exception ex)
{
// ex는 첫 번째 예외만
Console.WriteLine(ex.Message); // "A 실패"
// 모든 예외를 보려면 task를 직접 확인
var allExceptions = new[] { t1, t2 }
.Where(t => t.IsFaulted)
.SelectMany(t => t.Exception!.InnerExceptions);
}WhenAny — 가장 빠른 것 먼저
Task.WhenAny 는 여러 task 중 하나라도 완료되면 반환합니다. 타임아웃 구현이나 경쟁 조건에서 유용합니다.
var fetchTask = FetchDataAsync();
var timeoutTask = Task.Delay(TimeSpan.FromSeconds(5));
var completed = await Task.WhenAny(fetchTask, timeoutTask);
if (completed == timeoutTask)
throw new TimeoutException("5초 초과");
var result = await fetchTask; // 이미 완료됨동시성 제한 — SemaphoreSlim
WhenAll로 너무 많은 작업을 동시에 실행하면 외부 API나 DB에 과부하를 줄 수 있습니다. SemaphoreSlim 으로 동시 실행 수를 제한합니다.
var semaphore = new SemaphoreSlim(maxConcurrency: 5);
var tasks = urls.Select(async url =>
{
await semaphore.WaitAsync();
try
{
return await http.GetStringAsync(url);
}
finally
{
semaphore.Release();
}
});
string[] results = await Task.WhenAll(tasks);체크포인트
| 패턴 | 의미 |
|---|---|
| 먼저 task 생성 | 동시에 시작하게 만드는 핵심 |
Task.WhenAll(t1, t2) | 모두 완료까지 대기 |
Task.WhenAny(t1, t2) | 하나라도 완료되면 반환 |
AggregateException | 하나 실패 → 전체 실패, 모든 예외 포함 |
SemaphoreSlim | 동시 실행 수 제한 |
주의할 점
무제한 병렬화는 위험합니다. 1000개 URL을 동시에 요청하면 외부 API가 rate limit으로 차단하거나 DB 연결 풀이 고갈됩니다. SemaphoreSlim으로 동시 실행 수를 제한하거나, Parallel.ForEachAsync (C# 6+)의 MaxDegreeOfParallelism을 활용하세요.
task들 중 하나가 예외를 던지면 WhenAll 자체는 실패하지만 나머지 task들은 계속 실행됩니다. task를 취소하려면 CancellationToken을 각 task에 전달해야 합니다.
참고 링크
2 sources