ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Wait와 await의 차이 & Deadlock이 발생하는 경우 해결법
    카테고리 없음 2021. 9. 28. 01:26

    1번 코드:

    public async Task DoSomething()
    {
        await SomethingAwaitable();
        ...
    }
    
    public async void Button_Click() // 버튼 클릭으로 실행되는 함수
    {
        await DoSomething();
        ...
    }

    위와 같은 코드가 있다.

     

    위의 코드를 아래와 같이 변경하면 어떻게 될까?

     

    2번 코드:

    public async Task DoSomething()
    {
        await SomethingAwaitable();
        ...
    }
    
    public void Button_Click() // 버튼 클릭으로 실행되는 함수
    {
        DoSomething().Wait();
        ...
    }

    await 키워드를 빼버리고 DoSomething이 반환하는 Task를 Wait하도록 변경했다.

     

    await에 대해서 비동기를 동기처럼 사용할 수 있게 해준다는 정도로만 이해하고 있다면

    2번 코드도 잘 동작할 것이라고 생각할지 모른다.

    하지만 실제로 테스트를 해보면, 1번 코드와 달리 2번 코드는 버튼을 클릭하면 프로그램이 멈춰버린다.

    그렇다고 죽는것도 아니고 그냥 멈춰버려서 강제로 종료해야하는 상황이 된다.

     

    대체 왜 저런 일이 생기는 것일까?

     

    아래는 async await에 대해서 잘 설명한 인기 있는 블로그 글이다.

    Async and Await (stephencleary.com)

    Don't Block on Async Code (stephencleary.com)

    전체적으로 읽어보면 유익한 내용이 많으니 영어에 거부감이 없다면 쭉 읽어보면 좋겠다.

    이후의 설명들은 위 두 링크를 참고하여 작성하였다.

     

    아래 예시는 위 링크의 글에 나오는 예시를 살짝 변형한 것이다.

    // My "library" method.
    public static async Task<JObject> GetJsonAsync(Uri uri)
    {
      // (real-world code shouldn't use HttpClient in a using block; this is just example code)
      using (var client = new HttpClient())
      {
        var jsonString = await client.GetStringAsync(uri);
        return JObject.Parse(jsonString);
      }
    }
    
    // My "top-level" method.
    public async void Button1_Click(...)
    {
      textBox1.Text = await GetJsonAsync(...);
    }

    실행 순서는 다음과 같다:

    1. 버튼 클릭을 통해서 Button1_Click 함수를 실행하며 시작한다.

    2. 함수를 실행하자마자 바로 GetJsonAsync를 만난다. GetJsonAsync를 실행한다.

    3. async 함수인 GetJsonAsync는 일단 다른 함수들과 똑같이 순차적으로 실행된다.

    4. 그러다가 시간이 오래 걸리는 작업인 GetStringAsync를 만난다.

    5. GetStringAsync의 자세한 내용은 모르겠으나, await 키워드를 사용할 수 있는 것을 보아 awaitable을 반환하는 함수이다. 일단 작업이 완료되었든 완료되지 않았든 Task 타입의 반환값을 반환한다.

    6. GetStringAsync 함수의 앞에 붙은 await 키워드 때문에, 반환받은 awaitable값(여기서는 Task)이 완료되었는지 여부를 검사한다.

    7. 운이 좋게도 Task가 완료되었으면 GetJsonAsync 함수의 나머지 부분도 순차적으로 실행한다.

    8. 그렇게 GetJsonAsync가 순조롭게 끝나고, Button1_Click에 있는 await 키워드도 똑같이 Task가 완료되었는지를 검사한다.

    9. 운이 좋게도 GetJsonAsync의 Task도 완료되었기 때문에, 나머지 부분도 순조롭게 실행된다.

     

    하지만 위의 경우는 운이 좋은 경우이고,

    운이 좋지 않아서(라기보단 대부분의 경우) GetStringAsync가 반환하는 Task가 완료되지 않은 경우,

    7. await 키워드는 곧바로 await 키워드가 속한 async 함수(여기서는 GetJsonAsyn함수)를 끝내고 완료되지 않은 Task를 반환한다. 그리고 GetJsonAsync함수에서 아직 실행되지 못한 나머지 부분을 GetStringAsync의 Task가 완료되고나면 실행되도록 한다. 이 때, await 키워드가 실행되기 직전의 context를 capture해둔다. (Context는 작업을 나중에 마저 하기 위해 필요한 변수 값들을 기록해놓은 것 정도로 생각하면 된다. 위 링크의 글을 여러 번 읽어봤지만 저 capture한다는 말이 무슨 뜻으로 쓰였는지는 잘 모르겠다. 그냥 기억해둔다 정도로 생각하면 되려나...?)

    8. GetJsonAsync함수가 어쨌든 종료되었기 때문에, Button1_Click 함수의 await 키워드가 GetJsonAsync의 결과값을 검사한다.

    9. GetJsonAsync가 반환한 Task역시 완료되지 않았기 때문에, await 키워드에 의해서 Button1_Click 함수는 즉시 종료되고, 아직 실행되지 않은 나머지 부분은 자동적으로 Task가 완료되면 실행되도록 한다.

    10. 시간이 지나면 GetStringAsync가 완료될 것이다. 그러면 아까 7번 과정에서 GetJsonAsync함수의 실행되지 못한 나머지 부분이 실행된다. 이 나머지 작업은 위에서 capture해 두었던 context와 동일한 context에서 실행된다. 이 때, context만 동일하고, thread는 다를 수 있다.(Context에서 필요한 값들을 꺼내서 thread에서 작업을 실행.)

     

    요약하면,

    1. 즉시 반환하고 block하지 않는다. (Task.Wait는 task가 완료될 때 까지 thread를 block하고, task가 완료된 후에 순차적으로 나머지 부분을 실행해서 정상적으로 함수가 종료되면 결과값을 반환한다.)

    2. 작업이 완료된 후, 실행되지 않은 코드를 마저 실행할 때, await 키워드가 실행된 동일한 context에서 실행된다.

     

    이제 위의 두 가지 특징을 기억해두고 아래의 예시로 가보자.

    // My "library" method.
    public static async Task<JObject> GetJsonAsync(Uri uri)
    {
      // (real-world code shouldn't use HttpClient in a using block; this is just example code)
      using (var client = new HttpClient())
      {
        var jsonString = await client.GetStringAsync(uri);
        return JObject.Parse(jsonString);
      }
    }
    
    // My "top-level" method.
    public void Button1_Click(...)
    {
      var jsonTask = GetJsonAsync(...);
      textBox1.Text = jsonTask.Result;
    }

    바로 위에서 예시로 사용한 코드와 매우 유사하다.

    GetJsonAync 함수를 대기하기 위해 await 키워드를 쓰는 대신 Task.Result(Wait와 마찬가지로 Task가 완료될 때 까지 thread를 block한다)를 사용했다.

     

    겨우 이것만 다를 뿐인데 위의 함수를 실행하면 어딘가에서 멈춰서 더이상 진행되지 않는다.

     

    여기서 await와 Wait의 차이가 드러난다.

     

    위의 예시는 아래와 같은 순서로 실행된다.

    1. 버튼을 클릭하면 Button1_Click이 먼저 실행된다. 버튼 클릭은 Ui 부분이므로 ui thread에 의해서 ui context에서 실행된다.

    2. 순서대로 GetJsonAsync가 실행된다. 딱히 context를 바꿀 이유가 없으므로 역시나 ui context에서 실행된다.

    3. GetJsonAsync는 순서대로 실행되다가 await GetStringAsync(...)를 실행한다. 역시나 ui context에서 실행된다.

    4. GetStringAsync가 좀 오래 걸리는 작업이어서 아직 완료되지 않은 Task를 반환했다고 하자. 그러면 await 키워드 때문에 아직 완료되지 않은 Task를 반환하고, 아직 완료되지 않은 나머지 부분들을 Task가 완료되고나면 수행될 수 있도록 한다. 나머지 부분들은 동일한 ui context에서 실행될 것이다. ui에 관련된 작업은 없으므로 어느 thread에서 실행될지는 딱히 상관 없다.

    5. GetJsonAsync가 어쨌든 리턴되었으니, Button1_Click의 나머지 부분들이 실행된다. 그런데 Task.Result 때문에 GetJsonAsync가 완료될 때 까지 ui thread가 block된다. ui thread는 ui context를 사용중이다.

    6. 시간이 흘러 GetStringAsync가 완료되었다. GetJsonAsync의 아직 실행되지 않은 부분들이 ui context에서 실행되어야 할 차례이다. 그런데 GetJsonAsync가 완료되길 기다리는 ui thread가 ui context를 잡고 놔주지 않는다.

     

    ui context를 잡고 있는 ui thread가 GetJsonAsync의 완료를 기다리는데,

    GetJsonAsync가 완료되기 위해서는 ui context가 사용 가능한 상태가 되어야 한다.

    이렇게 ui thread는 영원히 멈춰버린다...

    이러한 상황을 deadlock이라고 한다.

     

    이를 해결하는 방법이 두 가지가 있다.

    1. 나머지 부분을 리턴할 때, context를 capture하지 않도록 한다.

    ConfigureAwait(false)를 사용하면 context를 capture하지 않을 수 있다.

    하지만 이 방법은 위험하므로 사용하지 말라고 한다.

    중요하지 않으니 넘어가겠다.

    궁금한 사람은 블로그의 두 글에서 ConfigureAwait를 검색해서 읽어보도록.

     

    2. Wait 대신 await를 사용한다.

    await 키워드를 사용하려면 호출하는 함수도 async 함수로 변경해야 한다.

    async함수를 호출하는 함수를 async로 바꾸고, 또 그 async 함수를 호출하는 쪽을 async로 바꿔주는 것을 계에에에속 한다.

     

    호출하는 쪽을 계에에에속 async함수로 바꾸는 것이 귀찮아서 편법으로 Task.Wait를 사용하다가 프로그램이 멈추는 경험을 하고 나서 좋은 방법이 없을까 찾기 위해 조사하기 시작했는데,

    결론은 계에에에속 async함수로 바꿔주는 것이 가장 좋은 방법이라고 한다.

     

    아래 링크의 글은 왜 await 키워드를 써야하는지를 심도있게 설명한 글이다.

    Understanding Async, Avoiding Deadlocks in C# | by Eke Péter | Rubrikk Group | Medium

    하지만 Thread pool, SynchronizationContext, TaskScheduler와 같은 어마무시한 키워드들이 나오니, 웬만한 내공이 아니면 그냥 가볍게 훑어보고 넘어가는 것이 좋겠다. (보지 않아도 된다.)

     

    댓글

Designed by Tistory.