[c#] 이 비동기 작업이 중단되는 이유는 무엇입니까?

나는 새로운 ‘C #을 사용하는 방법을 요구하는 다중 계층 닷넷 4.5 응용 프로그램이 asyncawait키워드 바로 중단한다는하고 그 이유를 볼 수 없습니다.

맨 아래에는 데이터베이스 유틸리티 OurDBConn(기본적으로 기본 DBConnectionDBCommand개체에 대한 래퍼)를 확장하는 비동기 메서드가 있습니다 .

public static async Task<T> ExecuteAsync<T>(this OurDBConn dataSource, Func<OurDBConn, T> function)
{
    string connectionString = dataSource.ConnectionString;

    // Start the SQL and pass back to the caller until finished
    T result = await Task.Run(
        () =>
        {
            // Copy the SQL connection so that we don't get two commands running at the same time on the same open connection
            using (var ds = new OurDBConn(connectionString))
            {
                return function(ds);
            }
        });

    return result;
}

그런 다음 느린 실행 합계를 얻기 위해 이것을 호출하는 중간 수준의 비동기 메서드가 있습니다.

public static async Task<ResultClass> GetTotalAsync( ... )
{
    var result = await this.DBConnection.ExecuteAsync<ResultClass>(
        ds => ds.Execute("select slow running data into result"));

    return result;
}

마지막으로 동기식으로 실행되는 UI 메서드 (MVC 작업)가 있습니다.

Task<ResultClass> asyncTask = midLevelClass.GetTotalAsync(...);

// do other stuff that takes a few seconds

ResultClass slowTotal = asyncTask.Result;

문제는 그것이 마지막 줄에 영원히 매달려 있다는 것입니다. 전화하면 똑같은 일을합니다 asyncTask.Wait(). 느린 SQL 메서드를 직접 실행하면 약 4 초가 걸립니다.

내가 기대하는 동작은에 도달했을 때 asyncTask.Result완료되지 않은 경우 완료 될 때까지 기다려야하고, 완료되면 결과를 반환해야한다는 것입니다.

디버거를 사용하여 단계를 수행하면 SQL 문이 완료되고 람다 함수가 완료되지만 return result;라인에 GetTotalAsync도달하지 않습니다.

내가 뭘 잘못하고 있는지 아십니까?

이 문제를 해결하기 위해 조사해야 할 위치에 대한 제안이 있습니까?

어딘가에 교착 상태가 될 수 있습니까? 그렇다면 직접 찾을 수있는 방법이 있습니까?



답변

네, 교착 상태입니다. 그리고 TPL의 일반적인 실수이므로 기분 나쁘지 마십시오.

을 작성할 때 await foo런타임은 기본적으로 메서드가 시작된 동일한 SynchronizationContext에서 함수의 연속을 예약합니다. 영어로 ExecuteAsyncUI 스레드에서 전화를 걸었다 고 가정 해 보겠습니다 . 쿼리는 스레드 풀 스레드에서 실행 Task.Run되지만 (를 호출했기 때문에 ) 결과를 기다립니다. 즉, 런타임은 ” return result;“줄을 스레드 풀로 다시 예약하는 대신 UI 스레드에서 다시 실행하도록 예약합니다.

그렇다면이 교착 상태는 어떻게 될까요? 이 코드가 있다고 상상해보십시오.

var task = dataSource.ExecuteAsync(_ => 42);
var result = task.Result;

따라서 첫 번째 줄은 비동기 작업을 시작합니다. 두 번째 줄 은 UI 스레드차단합니다 . 따라서 런타임이 UI 스레드에서 “결과 반환”줄을 다시 실행하려고 할 때 Result완료 될 때까지 실행할 수 없습니다 . 그러나 물론 반환이 발생할 때까지 결과를 제공 할 수 없습니다. 이중 자물쇠.

이것은 TPL 사용의 주요 규칙을 보여줍니다. .ResultUI 스레드 (또는 다른 멋진 동기화 컨텍스트)에서 사용할 때 Task가 의존하는 것이 UI 스레드에 예약되지 않도록주의해야합니다. 그렇지 않으면 악이 발생합니다.

그래서 당신은 무엇을합니까? 옵션 # 1은 모든 곳에서 사용 대기이지만 이미 옵션이 아닙니다. 두 번째 옵션은 단순히 await 사용을 중지하는 것입니다. 두 함수를 다음과 같이 다시 작성할 수 있습니다.

public static Task<T> ExecuteAsync<T>(this OurDBConn dataSource, Func<OurDBConn, T> function)
{
    string connectionString = dataSource.ConnectionString;

    // Start the SQL and pass back to the caller until finished
    return Task.Run(
        () =>
        {
            // Copy the SQL connection so that we don't get two commands running at the same time on the same open connection
            using (var ds = new OurDBConn(connectionString))
            {
                return function(ds);
            }
        });
}

public static Task<ResultClass> GetTotalAsync( ... )
{
    return this.DBConnection.ExecuteAsync<ResultClass>(
        ds => ds.Execute("select slow running data into result"));
}

차이점이 뭐야? 이제 아무데도 기다리지 않으므로 UI ​​스레드에 암시 적으로 예약되는 것이 없습니다. 단일 리턴이있는 이와 같은 간단한 메소드의 경우 ” var result = await...; return result“패턴 을 수행 할 필요가 없습니다 . 비동기 수정자를 제거하고 작업 객체를 직접 전달하십시오. 다른 것이 없다면 오버 헤드가 적습니다.

옵션 # 3은 대기가 UI 스레드로 다시 예약하지 않고 스레드 풀로만 예약하도록 지정하는 것입니다. 다음과 같은 ConfigureAwait방법 으로이 작업을 수행합니다 .

public static async Task<ResultClass> GetTotalAsync( ... )
{
    var resultTask = this.DBConnection.ExecuteAsync<ResultClass>(
        ds => return ds.Execute("select slow running data into result");

    return await resultTask.ConfigureAwait(false);
}

작업을 기다리는 것은 일반적으로 작업중인 경우 UI 스레드에 예약됩니다. 의 결과를 기다리는 ContinueAwait것은 어떤 컨텍스트에 있든 무시하고 항상 스레드 풀에 예약합니다. 이것의 단점은 당신이 뿌려해야 할 것입니다 어디서나 어떤 놓친 때문에, 당신의 .Result가 의존 모든 기능에 .ConfigureAwait또 다른 교착 상태의 원인이 될 수 있습니다.


답변

이것은 제가 제 블로그에서 설명하는 것처럼 전형적인 혼합 async교착 상태 시나리오 입니다. Jason은이를 잘 설명했습니다. 기본적으로 “컨텍스트”는 매번 저장되고 메서드 를 계속하는 데 사용됩니다 . 이 “문맥”은 현재가 아니면 현재 입니다. 때 방법 시도를 계속하는 첫 재진입 (이 경우, ASP.NET 포착 “컨텍스트” ). ASP.NET 은 컨텍스트에서 한 번에 하나의 스레드 만 허용하며 컨텍스트에는 이미 스레드가 있습니다.awaitasyncSynchronizationContextnullTaskSchedulerasyncSynchronizationContextSynchronizationContextTask.Result

이 교착 상태를 방지하는 두 가지 지침이 있습니다.

  1. async끝까지 사용하십시오 . 당신은 이것을 “할 수 없다”고 언급했지만, 그 이유는 확실하지 않습니다. .NET 4.5의 ASP.NET MVC는 확실히 async작업을 지원할 수 있으며 변경하기 어렵지 않습니다.
  2. ConfigureAwait(continueOnCapturedContext: false)가능한 한 많이 사용하십시오 . 이는 캡처 된 컨텍스트에서 재개하는 기본 동작을 재정의합니다.

답변

나는 동일한 교착 상태에 있었지만 동기화 메서드에서 비동기 메서드를 호출하는 경우에는 다음과 같이 작동했습니다.

private static SiteMetadataCacheItem GetCachedItem()
{
      TenantService TS = new TenantService(); // my service datacontext
      var CachedItem = Task.Run(async ()=>
               await TS.GetTenantDataAsync(TenantIdValue)
      ).Result; // dont deadlock anymore
}

이것이 좋은 접근 방식입니까, 어떤 아이디어입니까?


답변

허용 된 답변에 추가하기 위해 (댓글에 충분한 담당자가 아님) 이 예제와 같이 아래의 task.Result모든 이벤트가 있지만, 이벤트를 사용하여 차단할 때이 문제가 발생 await했습니다 ConfigureAwait(false).

public Foo GetFooSynchronous()
{
    var foo = new Foo();
    foo.Info = GetInfoAsync.Result;  // often deadlocks in ASP.NET
    return foo;
}

private async Task<string> GetInfoAsync()
{
    return await ExternalLibraryStringAsync().ConfigureAwait(false);
}

문제는 실제로 외부 라이브러리 코드에 있습니다. 비동기 라이브러리 메서드는 대기를 구성한 방법에 관계없이 호출 동기화 컨텍스트에서 계속하려고하여 교착 상태가 발생했습니다.

따라서 대답은 ExternalLibraryStringAsync원하는 연속 속성을 갖도록 외부 라이브러리 코드의 자체 버전을 롤링 하는 것이 었습니다.


역사적 목적을위한 오답

많은 고통과 고뇌 끝에이 블로그 게시물 ( ‘교착 상태’의 경우 Ctrl-f)에 묻힌 해결책을 찾았 습니다 . 그것은 task.ContinueWith베어 대신 사용을 중심으로 회전 합니다 task.Result.

이전 교착 상태의 예 :

public Foo GetFooSynchronous()
{
    var foo = new Foo();
    foo.Info = GetInfoAsync.Result;  // often deadlocks in ASP.NET
    return foo;
}

private async Task<string> GetInfoAsync()
{
    return await ExternalLibraryStringAsync().ConfigureAwait(false);
}

다음과 같이 교착 상태를 피하십시오.

public Foo GetFooSynchronous
{
    var foo = new Foo();
    GetInfoAsync()  // ContinueWith doesn't run until the task is complete
        .ContinueWith(task => foo.Info = task.Result);
    return foo;
}

private async Task<string> GetInfoAsync
{
    return await ExternalLibraryStringAsync().ConfigureAwait(false);
}


답변

빠른 답변 :이 줄을 변경

ResultClass slowTotal = asyncTask.Result;

ResultClass slowTotal = await asyncTask;

왜? .result를 사용하여 콘솔 응용 프로그램을 제외한 대부분의 응용 프로그램 내부에서 작업 결과를 가져 오지 마십시오. 그렇게하면 프로그램이 도착할 때 중단됩니다.

.Result를 사용하려면 아래 코드를 시도해 볼 수도 있습니다.

ResultClass slowTotal = Task.Run(async ()=>await asyncTask).Result;


답변