[C#] Async, Await를 이용한 비동기 프로그래밍

2014. 3. 26. 14:09Others/C# 일반

VisualStudio 2012, .NET Framework 4.5부터 지원하는 대표적인 기능 중 하나가 바로 비동기 프로그래밍 입니다. 여기서는 비동기 프로그래밍이 무엇인지 정리 드리고자 합니다..

비동기가 왜 필요할까?

웹 2.0 시대가 오면서 OpenAPI, 매쉬업을 통한 개발이 각광받게 되었습니다. 즉, 다른 웹사이트의 자원을 요청하고 가공하여 서비스를 제공하는 경우가 많아지게 된 것이죠. 하지만 이러한 외부 서비스 자원은 가장 느리고 불안정한 자원으로써 서비스의 대기시간이 길어지게 하는 원인으로 작용하게 됩니다. 이렇게 늘어난 서비스 대기시간은 필연적으로 병목을 발생시키고 이는 전체적인 Application의 반응성을 떨어뜨리게 됩니다. 급기야 사용자가 늘어날 수록 비용은 급상승하게 되고 경우에 따라서는 서비스 자체를 불가능하게 만들기도 합니다.

이러한 문제는 비동기 프로그래밍을 통해서 해소될 수 있는데, 비동기 프로그래밍을 위해서는 전문적인 지식과 복잡한 설계를 필요로 합니다. 따라서 고객의 만족을 위해 신뢰할 수 있는 소프트웨어 개발에 집중해야 할 개발자는 비동기 처리를 위해 복잡한 로직을 생산하고 많은 시간을 쓰게 됩니다.

따라서 .NET Framework 4.5부터는 이러한 문제를 해소하고자 async와 await를 통한 비동기 프로그래밍을 지원하고 있습니다. async와 await를 바탕으로 기존의 동기방식의 심플한 구조로 작성할 수 있게 지원하며, 기존에 반복적으로 작성하곤 했던 복잡한 기능들은 .NET Framework가 대신해주게 됩니다.

Application 구분 비동기 지원
Web access HttpClient , SyndicationClient
파일 입출력 StorageFile, StreamWriter, StreamReader, XmlReader
이미지 처리 MediaCapture, BitmapEncoder, BitmapDecoder
WCF 프로그래밍 동기, 비동기 처리를 위한 지원

Async와 Await

async와 await는 비동기로 수행될 메서드를 작성하는데 필요한 핵심적인 키워드 입니다. 이를 통해 비동기로 수행될 메서드를 기존의 동기 메서드 처럼 쉽게 직관적으로 작성할 수 있습니다. async, await를 사용한 메서드를 비동기 메서드(Async Method)라고 합니다. 아래는 비동기 메서드에 대한 간단한 예제입니다.

// 주목해야할 세가지
//  - 비동기를 수행하는 메서드는 반드시 aync keyword로 수식해야합니다. 
//  - async keyword가 수식된 메서드는 Async Method로써 반환형은 반드시
//    void, Task, Task<T> 중 하나여야 합니다. 여기에서는 Task<int>를
//    반환형으로 지정하였는데 이는 비동기 처리 작업 후 반환되는 데이터가 int이기 때문입니다.
//  - Convention으로써 Method명을 Async로 끝마침으로써 이 메서드가 비동기 메서드라는 것을 명시해주세요.
async Task<int> AccessTheWebAsync()
{
    HttpClient client = new HttpClient();

    //아래는 HttpClient에 추가된 GetStringAsync라는 비동기 메서드를 통해서
    //다른 웹사이트의 접근하는 예제입니다. 이 메서드는 Task<T>를 반환하는데
    //여기서 Task<T>은 비동기 작업을 나타내는 Type으로써 비동기로 진행 중인 작업정보를 제공합니다.
    //즉, 아래 Task<string>은 string을 반환하는 비동기 작업을 나타냅니다.
    Task<string> getStringTask = client.GetStringAsync("http://msdn.microsoft.com");

    //비동기 메서드 내에도 기존의 동기방식의 처리코드를 포함할 수 있습니다.
    DoIndependentWork();

    // Task<string> Type으로 지정된 getStringTask 변수 앞에
    // await를 지정하였습니다. 이는 변수 getStringTask가 가리키는
    // 비동기 작업이 완료될 때까지AccessTheWebAsync 메서드를 중단시킵니다.
    //  - 비동기 작업 getStringTask가 완료될 때 까지 기다립니다.
    //  - 기다리는 동안 현재 쓰레드의 제어는 AccessTheWebAsync를 호출한 부모에게 넘어갑니다.
    //  - getStringTask이 완료되기 전까지 작업이 재개되지 않습니다.
    //  - getStringTask의 작업이 완료되면 await 명령은 변수 urlContents에 작업결과를 담고
    //    중단된 시점부터 다시 작업을 재개시킵니다.
    string urlContents = await getStringTask;

    // 이 비동기 메서드가 완료되길 기다렸던 이 메서드의 호출자에게
    // 결과를 반환합니다.
    return urlContents.Length;
}

위 코드를 바탕으로 async method의 핵심을 요약하면 다음과 같습니다.

  • asnyc로 수식된 메서드는 비동기로 수행되는 메서드가 됩니다.
  • Convention에 의해 메서드명의 접미사는 Async가 됩니다.
  • 비동기 메서드는 다음과 같은 반환형 중 하나를 가집니다.
    • Task<T>: 만약 비동기 메서드의 수행 후 반환되는 데이터가 있다면 반환형을 T에 명시해야 합니다.
    • Task: 비동기 메서드 수행 후 반환되는 데이터가 없을 때 사용합니다.
    • void: 비동기 메서드가 이벤트 핸들러로 사용될 때 void로 명시합니다.
  • 비동기 메서드는 적어도 1개 이상의 await를 포함할 수 있습니다. 하지만 반드시 포함해야 하는 것은 아닙니다. await를 만나면 해당 지점에서 작업이 중단되고 제어는 비동기 메서드를 호출한 호출자에게 넘어가게 됩니다. 이와 별개로 비동기 작업은 따로 진행되게 됩니다. 비동기 작업을 끝마치면 중단된 지점으로부터 다시 작업을 재개하게 됩니다.

위의 예제와 같이 작성된 비동기 메서드는 다음과 같은 방법으로 사용될 수 있습니다.

//Event Handler에서 다음과 같이 처리하실 수 있습니다.
private async void Button_Click(object obj, EventArgs e)
{
    Task<int> accessTheWebTask = AccessTheWebAsync();
    string result = await accessTheWebTask;
    tbxAverageAge.Text = result;
}

예외처리와 반복문 처리

전통적인 비동기 처리방식으로는 예외처리와 루프처리는 매우 까다로운 작업 중 하나였습니다. 하지만 .NET Framework에서는 동기방식으로 처리하는 것과 동일한 방식으로 작성할 수 있으며 나머지 어렵고 복잡한 부분은 컴파일러가 알아서 처리합니다.

//비동기 방식으로 처리하기 매우 복잡했던 반복제어, 예외처리도
//.NET Framework에서는 기존 처리방법과 동일하게 작성하시면 됩니다.
async Task<int> GetAverageAgeAstnc(string[] userNameList)
{
   int averageAge = 0;
   HttpClient client = new HttpClient();
   string url = "http://example.com/GetAge?userName=";
   try
   {
      foreach(string userName in userNameList)
      {
         int age = await client.GetStringAsync(url + userName);
         averageAge += age;
      }
      return averageAge / userNameList.Length;
   }
   catch(Exception ex)
   {
      return null;
   }
}

실행 흐름

비동기 프로그래밍을 하기 위해 가장 중요한 것은 실행 흐름을 정확하게 이해하는 것입니다. 아래는 Windows Application에서 비동기 작업의 흐름에 대한 예제입니다.

  1. 이벤트 StartButton_Click에 의해 비동기 메서드 AccessTheWebAsync()가 호출됩니다.
  2. HttpClient 인스턴스를 생성하고 비동기 메서드 GetStringAsync()를 호출합니다.
  3. GetStringAsync() 메서드 내부적으로 주어진 주소로 요청을 보내고 응답이 올 때까지 기다리게 되며 기다리는 동안 실행 제어를 호출자에게 넘겨주면서, 현재 작업상태를 Task<string> Type으로 반환하게 됩니다. getStrignTask 변수는 진행 중인 비동기 작업에 대한 정보를 나타냅니다.
  4. 비동기 메서드 GetStringAsync() 이후의 작업인 DoIndepentWork()가 실행됩니다.
  5. DoIndependentWork()는 일반 메서드이므로 동기적으로 실행됩니다.
  6. await는 비동기 작업 getStringTask가 완료될 때까지 AccessTheWebAsync()의 실행을 중단시킵니다. 그리고 현재 쓰레드의 제어를 호출자에게 넘겨줍니다. 또한 현재 AccessTheWebAsync의 작업상태를 나타내는 Task<int>를 생성하게 되며 이를 호출자에게 전달해 줍니다. 
    주의: 비동기 작업 getStringTask가 이미 완료되었다면, 여기서 AccessTheWebAsync는 제어를 중단하지 않고 작업을 계속 이어가게 됩니다.
    제어를 넘겨받은 AccessTheWebAsync의 호출자는 비동기 작업이 완료될 때까지 다른 작업을 수행하게 됩니다.
  7. 비동기 작업 getStringTask의 작업이 완료되면 await는 getStringTask로부터 결과값을 가져와 변수 urlContents에 담고, 중단되었던 지점부터 작업을 재개 시킵니다.
  8. 최종 결과가 호출자에게 반환되게 됩니다.

위와 같은 작업 흐름을 명확하게 이해하는 것이 중요하며, 실제로 작성해보시길 권장 드립니다. 이와 관련한 상세한 예제코드와 설명은 다음 링크에서 확인하실 수 있습니다. (http://msdn.microsoft.com/ko-kr/library/hh873191.aspx)

API Async Methods

.NET Framework에서는 대부분의 시나리오에서 빈번하게 발생하는 요구사항을 해결할 수 있도록 비동기 프로그래밍을 위한 다양한 API를 제공하고 있으며 이를 통해서 비동기 처리가 가능합니다.

위 예제에서는 HttpClient의 GetStringAsync가 하나의 예로써 사용되었습니다. 이와 같은 비동기 API들은 Async라는 접미사를 가지고 있으며 반환형이 Task, Task<T>, void 중 하나이며 (awaitable)이라는 표시가 나타납니다.

따라서 비동기 처리가 필요한 경우 해당 Async 메서드가 있는지 확인하는 것이 필요합니다. 만약 제공하고 있다면 이를 async, await로 수식함으로써 간단하게 비동기 처리로 전환하실 수 있습니다. 예를 들어 파일을 읽을 경우 기존의 동기작업은 다음과 같이 작성될 수 있습니다.

public void ReadButton_Click(object sender, EventArgs e)
{
    string filePath = @"c:\tmp.txt";
    using (FileStream sourceStream = new FileStream(filePath,
        FileMode.Open, FileAccess.Read, FileShare.Read,
        bufferSize: 4096, useAsync: true))
    {
        StringBuilder sb = new StringBuilder();

        byte[] buffer = new byte[0x1000];
        int numRead;
        while ((numRead = sourceStream.Read(buffer, 0, buffer.Length)) != 0)
        {
            string text = Encoding.Unicode.GetString(buffer, 0, numRead);
            sb.Append(text);
        }

        tbxResult.Text = sb.ToString();
    }
}

이를 비동기로 전환하고자 할 경우 해당 ReadAsync 메서드와 async와 await를 통해 손쉽게 비동기로 바꿀 수 있습니다.

public async void ReadButton_Click(object sender, EventArgs e)
{
    string filePath = @"c:\tmp.txt";
    using (FileStream sourceStream = new FileStream(filePath,
        FileMode.Open, FileAccess.Read, FileShare.Read,
        bufferSize: 4096, useAsync: true))
    {
        StringBuilder sb = new StringBuilder();

        byte[] buffer = new byte[0x1000];
        int numRead;
        while ((numRead = await sourceStream.ReadAsync(buffer, 0, buffer.Length)) != 0)
        {
            string text = Encoding.Unicode.GetString(buffer, 0, numRead);
            sb.Append(text);
        }

        tbxResult.Text = sb.ToString();
    }
}

이와 관련한 자세한 정보는 http://msdn.microsoft.com/library/windows/apps/Hh452713%28v=win.10%29.aspx 에서 확인 하실 수 있습니다.

Threads

비동기 메서드에서 await는 작업을 중단하며 현재 쓰레드의 제어를 부모 메서드로 넘기게 됩니다. async, await 키워드는 추가적인 쓰레드를 생성하지 않으며 따라서 비동기 메서드는 자신만의 쓰레드를 가지지 않으므로 멀티쓰레딩에 대한 고려가 불필요합니다. 비동기 메서드는 Synchronization Contenxt에서 동작하며 그 컨텍스트의 자원을 사용합니다.

Task.Run을 통해서 작업을 백그라운드 쓰레드로 옮길 수 있지만, 백그라운 쓰레드는 해당 작업을 즉시 지원하지 않으며 사용가능해질 때 까지 기다립니다.

비동기 프로그래밍을 위해 async, await를 이용하는 것은 BackgroundWorker(현재 처리를 다른 분리된 쓰레드로 실행하는 Class)를 통한 접근보다 훨신 간결하고, 경쟁상태 등 복잡한 쓰레드에 대한 고려를 요구하지 않습니다.

async와 await

  • await는 async 메서드에서만 사용할 수 있습니다. 일반 메서드에서는 await는 단순한 식별자로 사용되게 됩니다.
  • await는 비동기 메서드 내에서 중단점을 지정하게 됩니다. await 키워드는 컴파일러에게 비동기 작업이 끝나기 전에는 현재 지점 이후로 진행할 수 없음을 알리고, 제어를 호출자에게 넘깁니다. await 키워드는 현재 실행을 잠시 보류하는 것이기 때문에, 제어가 호출자에게 넘어갈 때 finally 블럭이 실행되는 등의 경우는 발생하지 않습니다.
  • async 메서드에 await가 반드시 포함되어야 하는 것은 아닙니다. await가 없는 async 메서드는 동기 메서드와 동일하게 처리됩니다. 또한 컴파일러는 이에 대해 Waring을 생성합니다.

async, await에 대한 보다 자세한 정보는 다음 링크를 확인하세요.

반환형과 매개변수

async 메서드는 Task, Task<T>, void를 반환형으로 가질 수 있습니다. Task, Task<T>에는 async 메서드 뿐만 아니라 다른 방법으로도 생성될 수 있으며 모든 생성된 Task, Task<T>는 await가 적용될 수 있습니다.

반환된 Task는 진행 중인 작업을 나타냅니다. Task는 비동기로 진행 중인 작업상태정보, 최종 결과 정보, 실패했을 때 예외정보까지 포함하고 있습니다.

void 반환형은 보통 비동기 메서드가 이벤트 핸들로러 사용될 때 지정됩니다. 왜냐하면 이벤트 핸들러로 사용되는 delegate가 return type을 void로 정의하고 있기 때문입니다. 하지만 Task를 반환하지 않기 때문에 해당 비동기 메서드를 호출한 호출자는 비동기 메서드에서 발생하는 작업경과나 Exception에 대해서 알 수 가 없습니다.

비동기 메서드에는 out, ref와 같은 Call by reference 형태의 매개변수를 지정할 수 없습니다.

Naming Convention

비동기 메서드는 Async를 Convention에 의해 접미사로 가집니다. 하지만 event, base class, interface에는 이러한 Convention이 생략될 수 있습니다. 예를 들어 Button_Click Event를 Button_ClickAsync로 명시할 필요는 없습니다.

Refernece