C# 과 ASP.NET CORE를 공부하면서 가장 힘든 점 중 하나는 턱없는 한글 문서입니다.

그래서 비동기 함수에서 ConfigureAwait(false)를 왜 사용하는지에 대한 좋은 글이 있어서 번역하게 됐습니다.

저자의 동의를 얻어서 번역한 자료입니다!



다른 분들에게 도움이 되었으면 좋겠네요 :)



원본:

저자: Juan


본문:

.NET4.5  부터 async/await 를 도입하면서 asynchronous code를 작성하기가 많이 쉬워졌다.

Async/Await 키워드들은 synchronous 코드 와 비슷하고 컴파일러가 asynchrous 프로그래밍에서 처리하기 가장 어려운 부분을 처리해주면서코드 가독성과 프로그래머의 생산성을 향상시켰다.

Async 코드를 만들기가 얼마나 쉬운지 알아보기 위해 컨텐츠를 string으로 반환하는 curl 코드를 example로 만들어보자.

public async Task<string> DoCurlAsync()
{
    using(var httpClient = new HttpClient())
    using(var httpresponse = await httpclient.GetAsync(“https://www.bynder.com”))
    {
        return await httpResponse.Content.ReadAsStringAsync();
    }
}


우리는 bynder의 content를 가져오는 동안 다른 쓰레드를 호출하는 것을 막지 않는 비동기 호출을 했다.

이론적으로(상상 속에서) 사람들은 항상 우리가 만든 DoCurlAsync 함수를 아래와 같이 사용할 것 이다.

var bynderContnets = await DoCurlAsync();


하지만, 프로그래밍 세계는 이상과 거리가 멀다. 몇몇 사람들은 다음과 같이 사용할 수 있다.

var bynderContents = DoCurlAsync().Result

이렇게 코드를 작성하면 동기 방식으로 코드가 수행되기 때문에 curl 함수가 종료될 때 까지 다른 쓰레드를 호출 하는것을 막는다. 만약 콘솔 어플리케이션을 실행시키는 중이라면, 우리의 코드는 대부분 예상대로 실행될 것이다.

그러나 아래와같이, 만약 UI Application에서 수행이 되어진다면, 예를 들어서 button을 클릭했을 때 수행이 되는거라면

public void OnButtonClicked(object sender, RouteEventArgs e)
{
    var bynderContents = DoCurlAsync().Result;
}


이 어플리케이션은 동작하지 않을 것이고 deadlock 상태가 되어 버립니다. 물론, 우리가 만든 함수를 사용하는 사람들은 우리의 함수가 application을 응답하지 못하게 만들었다고 불평할 것 입니다.

이와 같은 상황의 문제를 해결하기 위해서 우리는 함수를 아래와 같이 다시 작성합니다.

public async Task<string> DoCurlAsync()
{
    using(var httpClient = new HttpClient())
    using(var httpresponse = await httpclient.GetAsync(“https://www.bynder.com”).ConfigureAwait(false))
    {
        return await httpResponse.Content.ReadAsStringAsync().ConfigureAwait(false);
    }


사실 , 첫번째의 비동기 구문에 ConfigureAwait(false) 를 붙이는 것으로 이 문제는 충분히 해결할 수 있습니다.

public async Task<string> DoCurlAsync()
{
    using(var httpClient = new HttpClient())
    using(var httpresponse = await httpclient.GetAsync(“https://www.bynder.com”).ConfigureAwait(false))
    {
        return await httpResponse.Content.ReadAsStringAsync();
    }

결론적으로, 항상 ConfigureAwait(false)를 사용하는 것은 우리가 원치 않은 상황을 막기위한 good practice 입니다.




이제, 우리는 UI 어플리케이션(대부분의 콘솔 어플리케이션이 아닌)에서 dead lock이 발생하는지 분석하고 왜 ConfigureAwait(false)과 이 문제를 해결하는지 분석해볼 것 입니다.


먼저, 우리는 UI 어플리케이션 어떻게 동작하는지 이해하는 작업이 필요합니다.

  • UI 응답을 위한 UI 스레드라는 하나의 스레드가 있습니다. UI는 오직 이 쓰레드를 호출해야만 업데이트 할 수 있습니다. 그래서 만약 에 쓰레드 호출이 막혀 있다면 어플리케이션은 응답하지 않은 것으로 보입니다.
  • UI 쓰레드는 수행할 notification과 action을 받을 message queue를 가지고 있습니다. Win32에서는 아래와 같이 표현할 수 있습니다.

    • while( (bRet = GetMessage( &msg, NULL, 0, 0 )) != 0)
      {
          // No errors are handled, for simplicity purposes.
         TranslateMessage(&msg);
         DispatchMessage(&msg);
      }

  • UI 쓰레드는 기본적으로 SynchronizationContext를 가지고 있습니다.
    • SynchronizationContext
      다양한 동기화 모델에서 동기화 컨텍스트를 전파하는 기본 기능을 제공해주는 class

만약에 SynchronizationContext가 있다면 (UI 스레드에서 코드가 실행된다면) await 이후의 코드들은 원래의 thread context에서 수행될 것 라고 기본적으로 예상할 수 있습니다.

만약, 아래 예제와 같이 우리가 UI 컴포넌트를 UI 스레드가 아닌 다른 스레드에서 수정하려고 한다면 System.InvalidOperationException이 발생할 것입니다.


public void OnButtonClicked(object sender, RouteEventArgs e)
{
    var bynderContents = await DoCurlAsync();
    myTextBlock.text = bynderContents;
}


다시 우리의 예제 코드를 돌아와보면 , 우리의 async DoCurlAsync 호출은 개념적으로 아래 코드와 같다.

var currentContext = SynchronizationContext.Current;
var httpResponseTask = httpClient.GetAsync("https://www.bynder.com");
httpResponseTask.ContinueWith(delegate
{
    if (currentContext == null)
    {
        return await httpResonse.Content.ReadAsStringAsync();
    }   
    else
    {
        currentContext.Post(delegate {
            await httpResonse.Content.ReadAsStringAsync();
        }, null);
     }
}, TaskScheduler.Current);


NOTE: 이 snippet은 await 구문을 사용했을 때 발생하는 것과 비슷한 버젼의 코드다. 이것은 resources들은 사용하고 closing하는 부분은 다루지 않았다.

Post 요청은 UI 스레드 메시지 펌프에 처리 될 메시지들을 보내고, 그래서 DoCurlAsync를 끝내기 위해서는 UI Thread가  await httpResonse.Content.ReadAsStringAsync();를 실행하는 것이 필수적이다.

그러나 , 아래와 같은 시나리오에서는

public void OnButtonClicked(object sender, RoutedEventArgs e)
{
    var bynderContents = DoCurlAsync().Result;
}

UI 스레드가 막혀 있으므로 instruction을 수행할 수없다. DoCurlAsync는 결코 끝나지 않으므로 dead lock상태에 빠진다. ConfigureAwait(false)는 await 후의 코드를 호출자 컨텍스트에서 수행할 필요가 없게 해준다. 그러므로 어떠한 deadlock도 피할 수 있다. 



이 글은 왜 비동기 함수를 호출 할때 ConfigureAwait(fasle)를 호출하는 것이 

Best Practice 인지 설명해줍니다.


그 이유가 궁금하셨던 분들이 계셨다면 도움이 되면 좋겠습니다.


감사합니다.







+ Recent posts