
지난 글에서는 async/await에 대해 배웠습니다.
await Task.Delay(3000);
여기서 await가 기다리는 '진동벨'의 정체는 정확히 무엇일까요?
Task.Delay(3000)에서 Task는 대체 무슨 역할을 하는 걸까요?
이번 글에서는 Task에 대해 깊이 파헤쳐 보겠습니다.
Task는 System.Threading.Tasks네임스페이스에 있는 클래스입니다.
이 객체는 '미래에 완료될 비동기 작업 하나'를 나타냅니다.
단순히 '작업'이라고 생각하면 Thread와 헷갈릴 수 있습니다.
하지만 Thread와 Task는 근본적으로 다릅니다.
Task는 '작업에 대한 정보'를 담은 '서약서'입니다.
이 '서약서'에는 다음과 같은 정보가 담겨 있습니다.
Status속성)IsCompleted속성)IsCompletedSuccessfully속성)IsFaulted, Exception속성)IsCanceled속성)await키워드는 바로 이 Task라는 '서약서'을 계속 지켜보다가,
작업 상태가 '완료'로 바뀌면 다음 코드를 실행시켜주는 역할을 합니다.
이 부분이 많은 분들이 헷갈리는 지점입니다. 비유를 통해 명확하게 구분해 봅시다.
비유: 레스토랑 주방
- 요리사(
Thread): 실제로 요리를 하는 사람입니다.- 주문서(
Task): '파스타 1개 만들기'라는 '작업 내용'이 적힌 종이입니다.- 주방장(
ThreadPool): 주문서(Task)가 들어오면,
놀고 있는 요리사(Thread)에게 주문서를 할당해 요리를 시킵니다.
Task를 사용하면 요리사(스레드)를 직접 다루는 복잡한 과정을 신경 쓸 필요가 없습니다.
그냥 '파스타 1개 만들기'라는 주문서(Task)만 주방장(ThreadPool)에게 던져주면,
주방장이 알아서 가장 효율적인 방식으로 요리사들을 관리하며 작업을 처리해 줍니다.
Task를 만드는 가장 일반적인 방법은 Task.Run()을 사용하는 것입니다.
이 메서드는 CPU 바운드 작업을 백그라운드 스레드(스레드 풀의 스레드)에서 실행시키고,
그 작업에 대한 '서약서'인 Task객체를 즉시 반환합니다.
[코드]
using System;
using System.Threading;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
Console.WriteLine("Main 스레드: 무거운 작업을 시작합니다.");
// Task.Run은 작업을 스레드 풀에 맡기고, 'Task' 객체를 바로 반환한다.
Task heavyTask = Task.Run(() =>
{
// 이 람다식 안의 코드가 백그라운드 스레드에서 실행됨
Console.WriteLine($"작업 스레드 ID: {Thread.CurrentThread.ManagedThreadId}");
long sum = 0;
for (long i = 0; i < 3_000_000_000; i++)
{
sum += i;
}
Console.WriteLine("작업 스레드: 무거운 작업 완료!");
});
Console.WriteLine($"Main 스레드: heavyTask의 상태는? -> {heavyTask.Status}");
Console.WriteLine("Main 스레드: 다른 일을 먼저 처리합니다...");
// ... 다른 코드 실행 ...
// 이제 작업이 끝날 때까지 기다린다.
await heavyTask;
Console.WriteLine($"Main 스레드: heavyTask의 상태는? -> {heavyTask.Status}");
Console.WriteLine("Main 스레드: 모든 작업이 완료되었습니다.");
}
}
[실행 결과]
Main 스레드: 무거운 작업을 시작합니다.
Main 스레드: heavyTask의 상태는? -> WaitingToRun
Main 스레드: 다른 일을 먼저 처리합니다...
작업 스레드 ID: 7
작업 스레드: 무거운 작업 완료!
Main 스레드: heavyTask의 상태는? -> RanToCompletion
Main 스레드: 모든 작업이 완료되었습니다.
Task.Run이 호출되자마자 Main스레드는 멈추지 않고 다음 코드를 실행합니다.
heavyTask라는 '서약서'만 받아두고, 실제 무거운 계산은 다른 스레드에 맡긴 것이죠.
그리고 await를 만나는 시점에서 비로소 작업이 끝나기를 기다립니다.
우리는 async/await의 '진동벨'이 사실은 Task라는 객체였음을 알게 되었습니다.
| 항목 | 설명 |
|---|---|
| Task란? | 비동기 작업의 상태와 진행 과정을 나타내는 '서약서' |
| Task vs Thread | Task는 '작업 지시서', Thread는 '일꾼', Task가 더 고수준의 추상화 |
| Task.Run() | CPU 바운드 작업을 스레드 풀에서 쉽게 실행하고 Task 객체를 얻는 방법 |