핵심 개념: Task.WaitAll과 Task.WhenAll
C#에서 여러 개의 비동기 작업(Task)을 동시에 실행한 뒤, 그 결과를 기다려야 하는 상황이 자주 발생합니다. 이때 사용할 수 있는 대표적인 메서드가 바로 Task.WaitAll과 Task.WhenAll입니다.
Task.WaitAll은 전달된 모든 작업이 실행을 마칠 때까지 현재 스레드를 차단(block)합니다. 반면 Task.WhenAll은 모든 작업이 완료되었을 때에만 자신도 완료되는 새로운 작업을 생성하는 메서드로, 호출 시점에는 스레드를 차단하지 않습니다.
아래 두 예제의 실행 결과를 비교해 보면 차이를 명확하게 알 수 있습니다.
- 예제 1 (Task.WhenAll): 다른 작업들이 아직 진행 중인데도 "Both Tasks Completed"(모든 작업 완료) 메시지가 먼저 출력됩니다. 즉, 실행 흐름이 막히지 않습니다.
- 예제 2 (Task.WaitAll): 모든 작업이 끝난 후에야 해당 메시지가 출력됩니다. 즉, WaitAll이 호출된 위치에서 실행이 차단됩니다.
예제 1 – Task.WhenAll 사용
static void Main(string[] args){
Task task1 = new Task(() =>{
for (var i = 0; i < 5; i++){
Console.WriteLine("Task 1 - iteration {0}", i);
Task.Delay(1000);
}
Console.WriteLine("Task 1 complete");
});
Task task2 = new Task(() =>{
for (var i = 0; i < 5; i++){
Console.WriteLine("Task 2 - iteration {0}", i);
Task.Delay(1000);
}
Console.WriteLine("Task 2 complete");
});
task1.Start();
task2.Start();
Console.WriteLine("Waiting for tasks to complete.");
Task.WhenAll(task1, task2);
Console.WriteLine("Both Tasks Completed.");
Console.ReadLine();
}실행 결과
Waiting for tasks to complete. Both Tasks Completed. Task 1 - iteration 0 Task 2 - iteration 0 Task 2 - iteration 1 Task 2 - iteration 2 Task 2 - iteration 3 Task 1 - iteration 1 Task 1 - iteration 2 Task 1 - iteration 3 Task 1 - iteration 4 Task 1 complete Task 2 - iteration 4 Task 2 complete
결과를 보면 "Both Tasks Completed."가 작업들의 출력보다 먼저 나타납니다. Task.WhenAll은 완료를 나타내는 작업만 반환할 뿐, 호출한 스레드를 기다리게 하지 않기 때문입니다.
예제 2 – Task.WaitAll 사용
static void Main(string[] args){
Task task1 = new Task(() =>{
for (var i = 0; i < 5; i++){
Console.WriteLine("Task 1 - iteration {0}", i);
Task.Delay(1000);
}
Console.WriteLine("Task 1 complete");
});
Task task2 = new Task(() =>{
for (var i = 0; i < 5; i++){
Console.WriteLine("Task 2 - iteration {0}", i);
Task.Delay(1000);
}
Console.WriteLine("Task 2 complete");
});
task1.Start();
task2.Start();
Console.WriteLine("Waiting for tasks to complete.");
Task.WaitAll(task1, task2);
Console.WriteLine("Both Tasks Completed.");
Console.ReadLine();
}실행 결과
Waiting for tasks to complete. Task 1 - iteration 0 Task 2 - iteration 0 Task 1 - iteration 1 Task 1 - iteration 2 Task 1 - iteration 3 Task 1 - iteration 4 Task 1 complete Task 2 - iteration 1 Task 2 - iteration 2 Task 2 - iteration 3 Task 2 - iteration 4 Task 2 complete Both Tasks Completed
이번에는 모든 작업이 완전히 끝난 후에 "Both Tasks Completed"가 출력됩니다. Task.WaitAll이 해당 지점에서 스레드를 차단하고 작업 종료를 기다렸기 때문입니다.
정리 및 실무 팁
- Task.WaitAll: 반환 타입이 void이며, 호출 스레드를 동기적으로 차단합니다. UI 스레드에서 사용하면 화면이 멈출 수 있으므로 주의해야 합니다.
- Task.WhenAll: 완료를 나타내는 Task를 반환하므로, 비동기 메서드에서
await Task.WhenAll(task1, task2);처럼 await와 함께 사용하는 것이 권장됩니다. 이 경우 스레드를 차단하지 않으면서도 모든 작업 완료를 기다릴 수 있습니다.
참고로 위 예제 코드의 Task.Delay(1000)는 await 되지 않았기 때문에 실제 지연 효과는 없습니다. 실제 지연이 필요하다면 await Task.Delay(1000) 또는 Thread.Sleep(1000)을 사용해야 합니다.