[C#] async & await

Donghee·2024년 7월 11일
0

개요

마이크로소프트 문서에 따르면, async은 메서드, 람다 식을 비동기로 지정하는 한정자이고, await은 피연산자가 나타내는 비동기 작업이 끝나기까지 기다리는 연산자이다. 그렇다면 비동기 작업이란 무엇이고, async와 awiat으로 비동기 메서드는 어떻게 작동시킬까?

동기/비동기

마이크로소프트 문서에 있는 예시를 보자.

  1. 커피 한 잔을 따릅니다.
  2. 팬을 가열한 다음 계란 두 개를 볶습니다.
  3. 베이컨 세 조각을 튀깁니다.
  4. 빵 두 조각을 굽습니다.
  5. 토스트에 버터와 잼을 바릅니다.
  6. 오렌지 주스 한잔을 따릅니다.

아침 식사를 위해 위 과정을 진행한다고 가정하자. 우리는 보통 이 과정을 비동기적으로 진행한다. 즉, 2번에서 팬을 가열하는 도중에 3번의 베이컨 준비를 시작하고, 3번의 베이컨을 튀기면서, 4번을 위해 토스트기에 빵을 넣는다. 이처럼 하나의 작업을 실행하고 세부 작업이 완료될때까지 다른 작업을 수행하는 것이 비동기 작업이다. 반대로 하나의 작업을 실행했을 때 그 작업만을 바라보고 끝날 때까지 아무것도 하지 않는 것을 동기 작업이라고 부른다.
이런 비동기 작업은 특히 처리 속도가 보장되지 않고 시간이 오래 걸릴 수 있는 웹/게임 서버 작업에서 많이 이용된다고 한다.

async와 await

그렇다면 async와 await은 무슨 역할을 할까? 내 친구 마이크로소프트 문서를 보자.

C#의 async 및 await 키워드는 비동기 프로그래밍의 핵심입니다. 이 두 개의 키워드를 사용하면 .NET Framework, .NET Core 또는 Windows 런타임의 리소스를 사용하여 동기 메서드를 만드는 것만큼 쉽게 비동기 메서드를 만들 수 있습니다.

다시 말해 C#에서 비동기 메서드를 편히 만드는 데에 꼭 필요한 키워드라는 것이다.

async

위에도 서술했듯이, async는 한정자로, 메서드나 람다 식을 비동기로 지정하는 역할을 한다. 마치 접근 한정자로 public이나 private을 붙이는 것처럼 붙여주면 된다.

public async Task<int> MethodAsync()
{
    //...
}

async 메서드, 즉 비동기 메서드의 반환 형식은 세가지이다.

  • void
  • Task
  • Task<T>

await

await은 단항 연산자로, 피연산자가 나타내는 비동기 작업이 완료될 때까지 동작을 일시 중단시킨다. 비동기 작업이 끝나는 것을 기다리게 해주는 키워드라고 보면 되겠다. 이 키워드를 실행하는 메서드는 async 메서드여야만 한다.

Task

async의 반환 형식으로 튀어나온 Task는 무엇일까?
Task는 async와 await보다 먼저 나온 개념으로, 비동기 작업들을 담고 있는 클래스이다. 위의 아침식사 예제 중 3번 베이컨 준비 동작을 비동기 메서드로 나타내본 코드다.

        private static async Task FryBaconAsync(int slices)
        {
            Console.WriteLine($"팬에 {slices} 개의 베이컨을 놓는다.");
            Console.WriteLine("베이컨 앞면 굽는 중...");
            await Task.Delay(3000);
            for (int slice = 0; slice < slices; slice++)
            {
                Console.WriteLine("베이컨 한 개를 뒤집는다.");
            }
            Console.WriteLine("베이컨 뒷면 굽는 중...");
            await Task.Delay(3000);
            Console.WriteLine("접시에 베이컨을 놓는다.");
        }

여기서 Task.Delay(int ms)는 안의 ms만큼을 대기하는 비동기 작업이다.

await Task.Delay(3000);

이것은 3초 동안 작업을 지연(Task.Delay(3000))시키고, 이 비동기 작업이 완료될 때까지 동작을 일시 중단(await)하는 것을 의미한다. 만약 Task.Delay(3000)만 써있었다면, 비동기적으로 작업을 지연하지 않을 것이다.

이렇게 Task를 반환하는 비동기 메서드는, Task 인스턴스로 받아올 수 있다. 예제로 살펴보자.

    class Program
    {
        public static async Task Main(string[] args)
        {
            System.Console.WriteLine("베이컨 조리 시작 전");
            
            Task bacon = FryBaconAsync(3);
            await bacon;
            
            System.Console.WriteLine("베이컨 조리 완료");
        }
 
        private static async Task FryBaconAsync(int slices)
        {
          	System.Console.WriteLine("베이컨 조리 시작 전");

          	Task bacon = FryBaconAsync(3);
          	await bacon;
          	System.Console.WriteLine("베이컨 조리 완료");

    		Console.ReadLine();
        }
    }

위와 같은 코드를 실행하면, 먼저 "베이컨 조리 시작 전"이 출력된다. 이후 Task 인스턴스인 bacon에 비동기 메서드인 FryBaconAsync가 저장되며 실행한다. 그 후 FryBaconAsync의 실행이 완료될때까지 대기한다. await을 통해 bacon의 동작을 실행함과 동시에 이를 기다리라고 명령했기 때문이다. 이후 동작이 완료되면 "베이컨 조리 완료"가 출력된다.

Task<T>

Task와 큰 차이가 없지만, 템플릿을 통해 반환 형식을 추가할 수 있다. 이것도 예제부터 보는 것이 빠르다.

    internal class Toast { }
    class Program
    {
        public static async Task Main(string[] args)
        {
            System.Console.WriteLine("토스트 조리 시작");
            
            Task<Toast> toast = ToastBreadAsync(2);
            Toast t = await toast;
            
            System.Console.WriteLine("토스트 조리 완료");
 
            Console.ReadLine();
        }
 
       	private static async Task<Toast> ToastBreadAsync(int slices)
        {
            for (int slice = 0; slice < slices; slice++)
            {
                Console.WriteLine("토스터기에 토스트 조각 하나씩 넣는다.");
            }
            Console.WriteLine("굽기 시작...");
            await Task.Delay(3000);
            Console.WriteLine("토스트기에서 토스트를 꺼낸다.");

            return new Toast();
        }

위의 아침식사 준비 예제에서 4번 토스트 굽기 과정을 나타낸 비동기 메서드이다. 비동기 작업이 끝날 시에 임의의 Toast라는 클래스를 반환하도록 Task<Toast>을 반환타입으로 작성했다.
Main에서는 ToastBreadAsync 작업이 실행되고, 이는 toast라는 Task<Toast> 객체에 저장된다. 이 작업을 await 시키면서, 반환되는 Toast 객체는 t에 저장하도록 해놓았다.

이런 식으로 비동기 작업에서 반환할 타입을 Task<T>를 통해 지정할 수 있다. 또한 이 반환 값은 await를 통해 받아올 수 있다.

한계

지금까지 알아본 예제들은 await를 통해 비동기 메서드가 끝나기만을 기다리는 코드다. 여러 작업을 실행해놓고 끝나기를 바라는 '진짜' 비동기 작업은 저 예시처럼 동작시키면 안된다.
우리가 원하는 '진짜' 비동기 작업은 여러 작업 중 끝나는 작업을 반환해주는 메서드인 Task.WhenAny(Task, Task, ...) 등을 이용해야 한다. 더 자세한 내용은 마이크로소프트 문서에 좋은 예시들이 많으니 참고하면 좋다.

참고

profile
마포고개발짱

0개의 댓글