C#의 Task 비동기 프로그래밍 모델The Task asynchronous programming model in C#

TAP(Task 비동기 프로그래밍) 모델은 비동기 코드에 대한 추상화를 제공합니다.The Task asynchronous programming model (TAP) provides an abstraction over asynchronous code. 항상 그렇듯이 코드는 일련의 명령문으로 작성합니다.You write code as a sequence of statements, just like always. 다음 명령문이 시작되기 전에 각 명령문이 완료되는 것처럼 해당 코드를 읽을 수 있습니다.You can read that code as though each statement completes before the next begins. 이러한 명령문 중 일부에서 작업을 시작하고 진행 중인 작업을 나타내는 Task를 반환할 수 있으므로 컴파일러는 여러 가지 변환을 수행합니다.The compiler performs a number of transformations because some of those statements may start work and return a Task that represents the ongoing work.

이 구문의 목표는 일련의 명령문처럼 읽지만 외부 리소스 할당과 작업 완료 시점에 따라 훨씬 더 복잡한 순서로 실행되는 코드를 사용하도록 설정하는 것입니다.That's the goal of this syntax: enable code that reads like a sequence of statements, but executes in a much more complicated order based on external resource allocation and when tasks complete. 사람이 비동기 작업이 포함된 프로세스에 대한 지침을 제공하는 방법과 비슷합니다.It's analogous to how people give instructions for processes that include asynchronous tasks. 이 문서에서는 asyncawait 키워드를 사용하여 일련의 비동기 명령이 포함된 코드를 쉽게 추론할 수 있는 방법을 알아보기 위해 아침 식사를 준비하기 위한 지침의 예를 사용합니다.Throughout this article, you'll use an example of instructions for making a breakfast to see how the async and await keywords make it easier to reason about code that includes a series of asynchronous instructions. 아침 식사를 준비하는 방법을 설명하기 위해 작성하는 지침은 다음 목록과 같습니다.You'd write the instructions something like the following list to explain how to make a breakfast:

  1. 커피 한 잔을 따릅니다.Pour a cup of coffee.
  2. 팬을 데운 다음, 계란 프라이 두 개를 만듭니다.Heat up a pan, then fry two eggs.
  3. 베이컨 세 조각을 튀깁니다.Fry three slices of bacon.
  4. 빵 두 조각을 굽습니다.Toast two pieces of bread.
  5. 토스트에 버터와 잼을 바릅니다.Add butter and jam to the toast.
  6. 오렌지 주스 한잔을 따릅니다.Pour a glass of orange juice.

요리에 대한 경험이 있는 경우 이러한 지침은 비동기적으로 실행됩니다.If you have experience with cooking, you'd execute those instructions asynchronously. 계란 프라이를 위해 팬을 데우기 시작한 다음, 베이컨을 시작합니다.You'd start warming the pan for eggs, then start the bacon. 토스터에 빵을 넣고 계란 프라이를 시작합니다.You'd put the bread in the toaster, then start the eggs. 프로세스의 각 단계에서 작업을 시작한 다음, 주의가 필요한 작업에 주의를 돌립니다.At each step of the process, you'd start a task, then turn your attention to tasks that are ready for your attention.

아침을 요리하는 것은 병렬로 수행되지 않는 비동기 작업의 좋은 예입니다.Cooking breakfast is a good example of asynchronous work that isn't parallel. 한 사람(또는 스레드)이 이러한 모든 작업을 처리할 수 있습니다.One person (or thread) can handle all these tasks. 아침 식사 비유를 계속하면 첫 번째 작업이 완료되기 전에 다음 작업을 시작하여 한 사람이 비동기적으로 아침 식사를 만들 수 있습니다.Continuing the breakfast analogy, one person can make breakfast asynchronously by starting the next task before the first completes. 누군가 보고 있는지 여부에 관계없이 요리는 계속됩니다.The cooking progresses whether or not someone is watching it. 계란 프라이를 위해 팬을 데우기 시작하자 말자 베이컨을 튀기기 시작할 수 있습니다.As soon as you start warming the pan for the eggs, you can begin frying the bacon. 베이컨 튀김이 시작되면 토스터에 빵을 넣을 수 있습니다.Once the bacon starts, you can put the bread into the toaster.

병렬 알고리즘의 경우 여러 요리사(또는 스레드)가 필요합니다.For a parallel algorithm, you'd need multiple cooks (or threads). 한 사람은 계란을 만들고 또 한 사람은 베이컨을 만드는 방식으로 진행될 것입니다.One would make the eggs, one the bacon, and so on. 즉 각각은 하나의 작업에만 집중할 것입니다.Each one would be focused on just that one task. 각 요리사(또는 스레드)는 베이컨이 뒤집을 준비가 되거나 토스트가 나올 때까지 동기적으로 차단됩니다.Each cook (or thread) would be blocked synchronously waiting for bacon to be ready to flip, or the toast to pop.

이제 C# 문으로 작성된 동일한 명령을 고려합니다.Now, consider those same instructions written as C# statements:

static void Main(string[] args)
{
    Coffee cup = PourCoffee();
    Console.WriteLine("coffee is ready");
    Egg eggs = FryEggs(2);
    Console.WriteLine("eggs are ready");
    Bacon bacon = FryBacon(3);
    Console.WriteLine("bacon is ready");
    Toast toast = ToastBread(2);
    ApplyButter(toast);
    ApplyJam(toast);
    Console.WriteLine("toast is ready");
    Juice oj = PourOJ();
    Console.WriteLine("oj is ready");

    Console.WriteLine("Breakfast is ready!");
}

컴퓨터에서는 사람들이 수행하는 것과 같은 방식으로 이러한 명령을 해석하지 않습니다.Computers don't interpret those instructions the same way people do. 다음 명령문으로 이동하기 전에 작업이 완료될 때까지 컴퓨터는 각 명령문에서 차단됩니다.The computer will block on each statement until the work is complete before moving on to the next statement. 이로 인해 불만족스러운 아침 식사를 만듭니다.That creates an unsatisfying breakfast. 이전 작업이 완료될 때까지 이후 작업을 시작할 수 없었습니다.The later tasks wouldn't be started until the earlier tasks had completed. 아침 식사를 만드는 데 훨씬 더 오래 걸리고, 일부 음식은 식은 채로 제공되었을 것입니다.It would take much longer to create the breakfast, and some items would have gotten cold before being served.

컴퓨터에서 위의 명령을 비동기적으로 실행하게 하려면 비동기 코드를 작성해야 합니다.If you want the computer to execute the above instructions asynchronously, you must write asynchronous code.

이러한 문제는 현재 작성하는 프로그램에 중요합니다.These concerns are important for the programs you write today. 클라이언트 프로그램을 작성할 때 UI에서 사용자 입력에 응답해야 합니다.When you write client programs, you want the UI to be responsive to user input. 웹에서 데이터를 다운로드하는 동안 애플리케이션에서 휴대폰이 중지된 것처럼 표시하면 안 됩니다.Your application shouldn't make a phone appear frozen while it's downloading data from the web. 서버 프로그램을 작성하는 경우 스레드가 차단되지 않도록 합니다.When you write server programs, you don't want threads blocked. 이러한 스레드는 다른 요청을 처리할 수 있습니다.Those threads could be serving other requests. 비동기 대안이 있을 때 동기 코드를 사용하면 비용이 적게 드는 규모 확장 기능이 저하됩니다.Using synchronous code when asynchronous alternatives exist hurts your ability to scale out less expensively. 차단된 스레드에 대한 비용을 지불합니다.You pay for those blocked threads.

성공적인 최신 애플리케이션에는 비동기 코드가 필요합니다.Successful modern applications require asynchronous code. 언어 지원 없이 비동기 코드를 작성하는 경우 콜백, 완료 이벤트 또는 코드의 원래 의도를 모호하게 하는 다른 수단이 필요했습니다.Without language support, writing asynchronous code required callbacks, completion events, or other means that obscured the original intent of the code. 동기 코드의 장점은 이해하기 쉽다는 것입니다.The advantage of the synchronous code is that it's easy to understand. 단계별 작업을 통해 쉽게 스캔하고 이해할 수 있습니다.The step-by-step actions make it easy to scan and understand. 기존의 비동기 모델에서는 코드의 기본 동작이 아니라 코드의 비동기적 특성에 집중할 수 밖에 없었습니다.Traditional asynchronous models forced you to focus on the asynchronous nature of the code, not on the fundamental actions of the code.

차단하는 대신 대기Don't block, await instead

앞의 코드에서는 동기 코드를 구성하여 비동기 작업을 수행하는 잘못된 사례를 보여 줍니다.The preceding code demonstrates a bad practice: constructing synchronous code to perform asynchronous operations. 작성한 대로 이 코드는 실행되는 스레드에서 다른 작업을 수행하지 못하도록 차단합니다.As written, this code blocks the thread executing it from doing any other work. 작업이 진행되는 동안에는 중단되지 않습니다.It won't be interrupted while any of the tasks are in progress. 마치 빵을 넣은 후 토스터를 쳐다보는 것과 같습니다.It would be as though you stared at the toaster after putting the bread in. 토스트가 나오기 전까지 아무하고도 대화하지 않을 것입니다.You'd ignore anyone talking to you until the toast popped.

먼저 이 코드를 업데이트하여 작업이 실행되는 동안 스레드가 차단되지 않도록 하겠습니다.Let's start by updating this code so that the thread doesn't block while tasks are running. await 키워드는 작업을 차단하지 않는 방식으로 시작한 다음, 해당 작업이 완료되면 실행을 계속합니다.The await keyword provides a non-blocking way to start a task, then continue execution when that task completes. 간단한 비동기 버전의 아침 식사 준비 코드는 다음과 같습니다.A simple asynchronous version of the make a breakfast code would look like the following snippet:

static async Task Main(string[] args)
{
    Coffee cup = PourCoffee();
    Console.WriteLine("coffee is ready");
    Egg eggs = await FryEggs(2);
    Console.WriteLine("eggs are ready");
    Bacon bacon = await FryBacon(3);
    Console.WriteLine("bacon is ready");
    Toast toast = await ToastBread(2);
    ApplyButter(toast);
    ApplyJam(toast);
    Console.WriteLine("toast is ready");
    Juice oj = PourOJ();
    Console.WriteLine("oj is ready");

    Console.WriteLine("Breakfast is ready!");
}

이 코드는 계란이나 베이컨을 요리하는 동안 차단되지 않습니다.This code doesn't block while the eggs or the bacon are cooking. 하지만 이 코드는 다른 작업을 시작하지 않습니다.This code won't start any other tasks though. 토스트가 토스터에 넣어져 나올 때까지 쳐다보고 있습니다.You'd still put the toast in the toaster and stare at it until it pops. 그러나 적어도 주의를 끌려고 하는 누구에게나 응답할 수는 있습니다.But at least, you'd respond to anyone that wanted your attention. 여러 주문을 받는 식당에서 요리사는 첫 번째 요리를 하는 동안 또 다른 아침 식사 준비를 시작할 수 있습니다.In a restaurant where multiple orders are placed, the cook could start another breakfast while the first is cooking.

시작했지만 아직 완료되지 않은 작업을 기다리는 동안 아침 식사 작업 스레드가 차단되지 않습니다.Now, the thread working on the breakfast isn't blocked while awaiting any started task that hasn't yet finished. 일부 애플리케이션의 경우 이 변경만으로 충분합니다.For some applications, this change is all that's needed. GUI 애플리케이션은 이 변경만으로도 사용자에게 응답합니다.A GUI application still responds to the user with just this change. 그러나 지금 시나리오에서는 더 많은 작업이 필요합니다.However, for this scenario, you want more. 각 구성 요소 작업이 순차적으로 실행되지 않도록 해야 합니다.You don't want each of the component tasks to be executed sequentially. 이전 작업이 완료되기를 기다리기 전에 각 구성 요소 작업을 시작하는 것이 좋습니다.It's better to start each of the component tasks before awaiting the previous task's completion.

동시에 작업 시작Start tasks concurrently

대부분의 시나리오에서는 독립적인 몇 가지 작업을 즉시 시작하려고 합니다.In many scenarios, you want to start several independent tasks immediately. 그런 다음, 각 작업이 완료되면 준비된 다른 작업을 계속할 수 있습니다.Then, as each task finishes, you can continue other work that's ready. 아침 식사 비유에서 이는 아침 식사를 더 빨리 준비하는 방법입니다.In the breakfast analogy, that's how you get breakfast done more quickly. 모든 것을 거의 동시에 완료할 수 있습니다.You also get everything done close to the same time. 이에 따라 따뜻한 아침 식사가 준비됩니다.You'll get a hot breakfast.

System.Threading.Tasks.Task 및 관련 형식은 진행 중인 작업을 추론하는 데 사용할 수 있는 클래스입니다.The System.Threading.Tasks.Task and related types are classes you can use to reason about tasks that are in progress. 이를 통해 아침 식사를 준비하는 방법과 더 비슷한 코드를 작성할 수 있습니다.That enables you to write code that more closely resembles the way you'd actually create breakfast. 계란, 베이컨 및 토스트 요리를 동시에 시작할 수 있을 것입니다.You'd start cooking the eggs, bacon, and toast at the same time. 각 요리에 필요한 작업이 있으므로 해당 작업에 주의를 기울이고, 다음 작업을 처리한 다음, 주의가 필요한 다른 작업을 기다립니다.As each requires action, you'd turn your attention to that task, take care of the next action, then await for something else that requires your attention.

작업을 시작하고, 해당 작업을 나타내는 Task 개체를 유지합니다.You start a task and hold on to the Task object that represents the work. 결과를 사용하기 전에 각 작업을 기다립니다(await).You'll await each task before working with its result.

아침 식사 코드를 이처럼 변경해 보겠습니다.Let's make these changes to the breakfast code. 첫 번째 단계는 작업을 기다리지 않고 시작될 때 해당 작업을 저장하는 것입니다.The first step is to store the tasks for operations when they start, rather than awaiting them:

Coffee cup = PourCoffee();
Console.WriteLine("coffee is ready");
Task<Egg> eggTask = FryEggs(2);
Egg eggs = await eggTask;
Console.WriteLine("eggs are ready");
Task<Bacon> baconTask = FryBacon(3);
Bacon bacon = await baconTask;
Console.WriteLine("bacon is ready");
Task<Toast> toastTask = ToastBread(2);
Toast toast = await toastTask;
ApplyButter(toast);
ApplyJam(toast);
Console.WriteLine("toast is ready");
Juice oj = PourOJ();
Console.WriteLine("oj is ready");

Console.WriteLine("Breakfast is ready!");

다음으로, 아침 식사를 제공하기 전에 베이컨과 달걀에 대한 await 문을 메서드 끝으로 이동할 수 있습니다.Next, you can move the await statements for the bacon and eggs to the end of the method, before serving breakfast:

Coffee cup = PourCoffee();
Console.WriteLine("coffee is ready");
Task<Egg> eggTask = FryEggs(2);
Task<Bacon> baconTask = FryBacon(3);
Task<Toast> toastTask = ToastBread(2);
Toast toast = await toastTask;
ApplyButter(toast);
ApplyJam(toast);
Console.WriteLine("toast is ready");
Juice oj = PourOJ();
Console.WriteLine("oj is ready");

Egg eggs = await eggTask;
Console.WriteLine("eggs are ready");
Bacon bacon = await baconTask;
Console.WriteLine("bacon is ready");

Console.WriteLine("Breakfast is ready!");

앞의 코드가 더 잘 작동합니다.The preceding code works better. 모든 비동기 작업을 한 번에 시작합니다.You start all the asynchronous tasks at once. 결과가 필요할 때만 각 작업을 기다립니다.You await each task only when you need the results. 앞의 코드는 다른 마이크로서비스를 요청한 다음, 결과를 단일 페이지로 결합하는 웹 애플리케이션의 코드와 비슷할 수 있습니다.The preceding code may be similar to code in a web application that makes requests of different microservices, then combines the results into a single page. 모든 요청을 즉시 수행한 다음, 이러한 모든 작업을 기다리고(await) 웹 페이지를 구성합니다.You'll make all the requests immediately, then await all those tasks and compose the web page.

작업 구성Composition with tasks

토스트를 제외한 모든 아침 식사가 동시에 준비되었습니다.You have everything ready for breakfast at the same time except the toast. 토스트를 만드는 것은 비동기 작업(빵 굽기)과 동기 작업(버터와 잼 바르기)의 구성입니다.Making the toast is the composition of an asynchronous operation (toasting the bread), and synchronous operations (adding the butter and the jam). 이 코드를 업데이트하면 중요한 개념을 알 수 있습니다.Updating this code illustrates an important concept:

중요

동기 작업이 뒤따르는 비동기 작업으로 구성된 작업은 비동기 작업입니다.The composition of an asynchronous operation followed by synchronous work is an asynchronous operation. 즉 작업의 일부가 비동기이면 전체 작업이 비동기입니다.Stated another way, if any portion of an operation is asynchronous, the entire operation is asynchronous.

이전 코드에서는 Task 또는 Task<TResult> 개체를 사용하여 실행 중인 작업을 유지할 수 있음을 보여 주었습니다.The preceding code showed you that you can use Task or Task<TResult> objects to hold running tasks. 결과를 사용하기 전에 각 작업을 기다립니다(await).You await each task before using its result. 다음 단계는 다른 작업의 결합을 나타내는 메서드를 만드는 것입니다.The next step is to create methods that represent the combination of other work. 아침 식사를 제공하기 전에 빵을 구운 후에 버터와 잼을 바르는 것을 나타내는 작업을 기다리려고 합니다.Before serving breakfast, you want to await the task that represents toasting the bread before adding butter and jam. 이 작업은 다음 코드를 사용하여 나타낼 수 있습니다.You can represent that work with the following code:

async Task<Toast> MakeToastWithButterAndJamAsync(int number)
{
    var toast = await ToastBreadAsync(number);
    ApplyButter(toast);
    ApplyJam(toast);
    return toast;
}

앞의 메서드에서 해당 시그니처에는 async 한정자가 있습니다.The preceding method has the async modifier in its signature. 이 경우 이 메서드에서 비동기 작업이 포함된 await 문을 포함하고 있다고 컴파일러에 알립니다.That signals to the compiler that this method contains an await statement; it contains asynchronous operations. 이 메서드는 빵을 구운 다음, 버터와 잼을 바르는 작업을 나타내며,This method represents the task that toasts the bread, then adds butter and jam. 이러한 세 가지 작업의 구성을 나타내는 Task<TResult>를 반환합니다.This method returns a Task<TResult> that represents the composition of those three operations. 이제 main 코드 블록은 다음과 같습니다.The main block of code now becomes:

static async Task Main(string[] args)
{
    Coffee cup = PourCoffee();
    Console.WriteLine("coffee is ready");
    var eggsTask = FryEggsAsync(2);
    var baconTask = FryBaconAsync(3);
    var toastTask = MakeToastWithButterAndJamAsync(2);

    var eggs = await eggsTask;
    Console.WriteLine("eggs are ready");
    var bacon = await baconTask;
    Console.WriteLine("bacon is ready");
    var toast = await toastTask;
    Console.WriteLine("toast is ready");
    Juice oj = PourOJ();
    Console.WriteLine("oj is ready");

    Console.WriteLine("Breakfast is ready!");

    async Task<Toast> MakeToastWithButterAndJamAsync(int number)
    {
        var toast = await ToastBreadAsync(number);
        ApplyButter(toast);
        ApplyJam(toast);
        return toast;
    }
}

앞의 변경에서는 비동기 코드를 사용하는 데 있어 중요한 기술을 보여 주었습니다.The previous change illustrated an important technique for working with asynchronous code. 작업을 반환하는 새 메서드로 구분하여 작업을 구성합니다.You compose tasks by separating the operations into a new method that returns a task. 해당 작업을 기다리는 시기를 선택할 수 있습니다.You can choose when to await that task. 다른 작업을 동시에 시작할 수 있습니다.You can start other tasks concurrently.

효율적인 작업 대기Await tasks efficiently

Task 클래스의 메서드를 사용하여 앞의 코드 끝에 있는 일련의 await 문을 향상시킬 수 있습니다.The series of await statements at the end of the preceding code can be improved by using methods of the Task class. 이러한 API 중 하나인 WhenAll은 다음 코드와 같이 인수 목록의 모든 작업이 완료되면 완료된 Task를 반환합니다.One of those APIs is WhenAll, which returns a Task that completes when all the tasks in its argument list have completed, as shown in the following code:

await Task.WhenAll(eggTask, baconTask, toastTask);
Console.WriteLine("eggs are ready");
Console.WriteLine("bacon is ready");
Console.WriteLine("toast is ready");
Console.WriteLine("Breakfast is ready!");

또 다른 옵션으로, 인수가 완료되면 완료된 Task<Task>를 반환하는 WhenAny를 사용하는 것입니다.Another option is to use WhenAny, which returns a Task<Task> that completes when any of its arguments completes. 반환된 작업은 이미 완료되었음을 알고 있으므로 기다릴 수 있습니다.You can await the returned task, knowing that it has already finished. 다음 코드에서는 WhenAny를 사용하여 첫 번째 작업이 완료될 때까지 기다린 다음, 결과를 처리하는 방법을 보여 줍니다.The following code shows how you could use WhenAny to await the first task to finish and then process its result. 완료된 작업의 결과가 처리되면 완료된 작업을 WhenAny에 전달된 작업 목록에서 제거합니다.After processing the result from the completed task, you remove that completed task from the list of tasks passed to WhenAny.

var allTasks = new List<Task>{eggsTask, baconTask, toastTask};
while (allTasks.Any())
{
    Task finished = await Task.WhenAny(allTasks);
    if (finished == eggsTask)
    {
        Console.WriteLine("eggs are ready");
    }
    else if (finished == baconTask)
    {
        Console.WriteLine("bacon is ready");
    }
    else if (finished == toastTask)
    {
        Console.WriteLine("toast is ready");
    }
    allTasks.Remove(finished);
}
Console.WriteLine("Breakfast is ready!");

이러한 모든 변경 이후에 Main의 최종 버전은 다음과 같습니다.After all those changes, the final version of Main looks like the following code:

static async Task Main(string[] args)
{
    Coffee cup = PourCoffee();
    Console.WriteLine("coffee is ready");
    var eggsTask = FryEggsAsync(2);
    var baconTask = FryBaconAsync(3);
    var toastTask = MakeToastWithButterAndJamAsync(2);

    var allTasks = new List<Task>{eggsTask, baconTask, toastTask};
    while (allTasks.Any())
    {
        Task finished = await Task.WhenAny(allTasks);
        if (finished == eggsTask)
        {
            Console.WriteLine("eggs are ready");
        }
        else if (finished == baconTask)
        {
            Console.WriteLine("bacon is ready");
        }
        else if (finished == toastTask)
        {
            Console.WriteLine("toast is ready");
        }
        allTasks.Remove(finished);
    }
    Console.WriteLine("Breakfast is ready!");

    async Task<Toast> MakeToastWithButterAndJamAsync(int number)
    {
        var toast = await ToastBreadAsync(number);
        ApplyButter(toast);
        ApplyJam(toast);
        return toast;
    }
}

이 최종 코드는 비동기입니다.This final code is asynchronous. 이 코드는 아침 식사를 요리하는 방법을 더 정확하게 반영하고 있습니다.It more accurately reflects how a person would cook a breakfast. 앞의 코드를 이 문서의 첫 번째 코드 샘플과 비교해 보세요.Compare the preceding code with the first code sample in this article. 핵심 작업은 코드를 읽어 파악할 수 있습니다.The core actions are still clear from reading the code. 이 코드는 이 문서의 시작 부분에 나와 있는 아침 식사 준비 지침을 읽는 것과 동일한 방식으로 읽을 수 있습니다.You can read this code the same way you'd read those instructions for making a breakfast at the beginning of this article. asyncawait 언어 기능을 사용하면 모든 사용자가 작성된 이러한 지침을 따를 수 있습니다. 가능한 한 작업을 시작하지만 작업이 완료될 때까지 기다리는 것을 차단하지 않도록 합니다.The language features for async and await provide the translation every person makes to follow those written instructions: start tasks as you can and don't block waiting for tasks to complete.