[c#] 교착 상태를 일으키는 비동기 / 대기 예제

C #의 async/ await키워드를 사용하는 비동기 프로그래밍에 대한 몇 가지 모범 사례를 보았습니다 (C # 5.0을 처음 사용했습니다).

주어진 조언 중 하나는 다음과 같습니다.

안정성 : 동기화 컨텍스트 파악

… 일부 동기화 컨텍스트는 재진입이 불가능하고 단일 스레드입니다. 이는 주어진 시간에 하나의 작업 단위 만 컨텍스트에서 실행될 수 있음을 의미합니다. 이에 대한 예는 Windows UI 스레드 또는 ASP.NET 요청 컨텍스트입니다. 이러한 단일 스레드 동기화 컨텍스트에서 교착 상태가되기 쉽습니다. 단일 스레드 컨텍스트에서 작업을 생성 한 다음 컨텍스트에서 해당 작업을 기다리면 대기 코드가 백그라운드 작업을 차단할 수 있습니다.

public ActionResult ActionAsync()
{
    // DEADLOCK: this blocks on the async task
    var data = GetDataAsync().Result;

    return View(data);
}

private async Task<string> GetDataAsync()
{
    // a very simple async method
    var result = await MyWebService.GetDataAsync();
    return result.ToString();
}

내가 직접 해부하려고하면 메인 스레드가에서 새 스레드로 생성 MyWebService.GetDataAsync();되지만 메인 스레드가 거기에서 기다리고 있기 때문에 GetDataAsync().Result. 한편, 데이터가 준비되었다고 가정하십시오. 메인 스레드가 계속 논리를 계속하지 않고 문자열 결과를 반환하는 이유는 GetDataAsync()무엇입니까?

누군가가 위의 예에서 교착 상태가 발생한 이유를 설명해 주시겠습니까? 나는 문제가 무엇인지에 대해 완전히 단서가 없습니다 …



답변

이 예를 살펴보면 Stephen이 명확한 답변을 제공합니다.

따라서 이것은 최상위 방법 ( Button1_ClickUI MyController.Get용 /ASP.NET 용) 부터 시작됩니다 .

  1. 최상위 메서드 호출 GetJsonAsync(UI / ASP.NET 컨텍스트 내).

  2. GetJsonAsync호출하여 REST 요청을 시작합니다 HttpClient.GetStringAsync(여전히 컨텍스트 내에서).

  3. GetStringAsyncTaskREST 요청이 완료되지 않았 음을 나타내는 uncompleted를 반환합니다 .

  4. GetJsonAsync에서 Task반환 된을 기다립니다 GetStringAsync. 컨텍스트가 캡처되고 GetJsonAsync나중에 메서드 를 계속 실행하는 데 사용됩니다 . 메서드가 완료되지 않았 음을 나타내는 GetJsonAsyncuncompleted를 반환합니다 .TaskGetJsonAsync

  5. 최상위 메서드는에서 Task반환 된를 동 기적으로 차단합니다 GetJsonAsync. 이것은 컨텍스트 스레드를 차단합니다.

  6. … 결국 REST 요청이 완료됩니다. 이것 Task으로에서 반환 된 이 완료 됩니다 GetStringAsync.

  7. 에 대한 연속 GetJsonAsync은 이제 실행할 준비가되었으며 컨텍스트에서 실행할 수 있도록 컨텍스트를 사용할 수있을 때까지 기다립니다.

  8. 교착 상태 . 최상위 메소드는 컨텍스트 스레드를 차단하고 GetJsonAsync완료 GetJsonAsync를 기다리고 있으며 컨텍스트가 완료 될 수 있도록 비어있을 때까지 기다리고 있습니다. UI 예제에서 “context”는 UI 컨텍스트입니다. ASP.NET 예제의 경우 “context”는 ASP.NET 요청 컨텍스트입니다. 이러한 유형의 교착 상태는 “컨텍스트”에 대해 발생할 수 있습니다.

읽어야 할 또 다른 링크 : Await, UI 및 deadlocks! 어머!


답변

  • 사실 1 : GetDataAsync().Result;반환 된 작업이 GetDataAsync()완료되면 실행 되고 그 동안 UI 스레드가 차단됩니다.
  • 사실 2 : await ( return result.ToString()) 의 연속은 실행을 위해 UI 스레드에 대기합니다.
  • 사실 3 :에서 반환 된 작업 GetDataAsync()은 대기중인 연속 작업이 실행될 때 완료됩니다.
  • 사실 4 : UI 스레드가 차단 되었기 때문에 대기중인 연속이 실행되지 않습니다 (사실 1).

이중 자물쇠!

교착 상태는 팩트 1 또는 팩트 2를 피하기 위해 제공된 대안으로 해제 될 수 있습니다.

  • 1,4를 피하십시오. UI 스레드를 차단하는 대신를 var data = await GetDataAsync()사용하면 UI 스레드가 계속 실행됩니다.
  • 2,3을 피하십시오. 차단되지 않은 다른 스레드에 대기의 연속을 큐에 넣습니다. 예를 들어 var data = Task.Run(GetDataAsync).Result스레드 풀 스레드의 동기화 컨텍스트에 연속을 게시하는 use . 이를 통해에서 반환 된 작업 GetDataAsync()을 완료 할 수 있습니다 .

이것은 Stephen Toub기사 에서 매우 잘 설명 되어 DelayAsync()있습니다.


답변

ASP.NET MVC 프로젝트에서이 문제를 다시 다루고있었습니다. async에서 메서드 를 호출하려는 PartialView경우 PartialView async. 그렇게하면 예외가 발생합니다.

async동기화 메서드에서 메서드 를 호출하려는 시나리오에서 다음과 같은 간단한 해결 방법을 사용할 수 있습니다 .

  1. 통화하기 전에 SynchronizationContext
  2. 전화 해, 여기에 더 이상 교착 상태가 없을 것입니다. 완료 될 때까지 기다리십시오.
  3. 복원 SynchronizationContext

예:

public ActionResult DisplayUserInfo(string userName)
{
    // trick to prevent deadlocks of calling async method 
    // and waiting for on a sync UI thread.
    var syncContext = SynchronizationContext.Current;
    SynchronizationContext.SetSynchronizationContext(null);

    //  this is the async call, wait for the result (!)
    var model = _asyncService.GetUserInfo(Username).Result;

    // restore the context
    SynchronizationContext.SetSynchronizationContext(syncContext);

    return PartialView("_UserInfo", model);
}


답변

또 다른 요점은 작업을 차단해서는 안되며 교착 상태를 방지하기 위해 비동기식을 사용하는 것입니다. 그러면 모두 비동기식이 아닌 비동기식 차단이됩니다.

public async Task<ActionResult> ActionAsync()
{

    var data = await GetDataAsync();

    return View(data);
}

private async Task<string> GetDataAsync()
{
    // a very simple async method
    var result = await MyWebService.GetDataAsync();
    return result.ToString();
}


답변

내가 찾은 해결 Join방법은 결과를 요청하기 전에 작업에 확장 방법 을 사용하는 것입니다.

코드는 다음과 같습니다.

public ActionResult ActionAsync()
{
  var task = GetDataAsync();
  task.Join();
  var data = task.Result;

  return View(data);
}

결합 방법은 다음과 같습니다.

public static class TaskExtensions
{
    public static void Join(this Task task)
    {
        var currentDispatcher = Dispatcher.CurrentDispatcher;
        while (!task.IsCompleted)
        {
            // Make the dispatcher allow this thread to work on other things
            currentDispatcher.Invoke(delegate { }, DispatcherPriority.SystemIdle);
        }
    }
}

이 솔루션의 단점을 파악하기에 도메인에 충분하지 않습니다 (있는 경우).


답변