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


102

나는 새로운 '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도달하지 않습니다.

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

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

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

답변:


150

네, 교착 상태입니다. 그리고 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또 다른 교착 상태의 원인이 될 수 있습니다.


6
BTW, 질문은 ASP.NET에 관한 것이므로 UI ​​스레드가 없습니다. 그러나 교착 상태의 문제는 ASP.NET으로 인해 정확히 동일합니다 SynchronizationContext.
svick

문제가 없지만 async/ await키워드 없이 TPL을 사용하는 유사한 .Net 4 코드가 있었기 때문에 그것은 많이 설명했습니다 .
Keith


: 누구는 여기에 설명되어 있습니다 (나 같은)를 VB.net 코드를 찾고 있다면 docs.microsoft.com/en-us/dotnet/visual-basic/programming-guide/...
MichaelDarkBlue

당신의 도움이 나를 기쁘게 할 수 stackoverflow.com/questions/54360300/...
Jitendra Pancholi

36

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

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

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

ConfigureAwait(false)현재 기능이 다른 컨텍스트에서 재개된다는 것을 보장 합니까 ?
chue x

MVC 프레임 워크가이를 지원하지만 이것은 많은 클라이언트 측 JS가 이미 존재하는 기존 MVC 앱의 일부입니다. async클라이언트 측에서 작동하는 방식을 깨지 않고 는 작업으로 쉽게 전환 할 수 없습니다 . 나는 확실히 그 옵션을 장기적으로 조사 할 계획입니다.
Keith

내 의견을 명확히하기 위해- ConfigureAwait(false)콜 트리를 사용 하면 OP의 문제가 해결 될지 궁금했습니다 .
chue x

3
@Keith : MVC 액션을 만드는 것은 async클라이언트 측에 전혀 영향을주지 않습니다. 다른 블로그 게시물 인 asyncDoes n't Change the HTTP Protocol 에서 이에 대해 설명합니다 .
Stephen Cleary 2013 년

1
@Keith : async코드베이스를 통해 "성장"하는 것은 정상입니다 . 컨트롤러 방법은 비동기 작업에 따라 달라질 수 있습니다 경우, 기본 클래스 메서드는 해야한다 반환 Task<ActionResult>. 코드를 async혼합 async하고 동기화하는 것이 어렵고 까다롭기 때문에 대규모 프로젝트를로 전환하는 것은 항상 어색 합니다. 순수한 async코드는 훨씬 간단합니다.
Stephen Cleary

12

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

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

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


이 솔루션은 저에게도 효과가 있지만 좋은 솔루션인지 또는 어딘가에서 깨질 수 있는지 확실하지 않습니다. 누구는 설명 할 수
콘스탄틴 Vdovkin

물론 마지막으로 나는이 솔루션과 함께 가서는 ..... 문제없이 생산적인 환경에서 일하고
Danilow

1
Task.Run을 사용하여 성능 저하를 겪고 있다고 생각합니다. 내 테스트에서 Task.Run은 100ms http 요청에 대한 실행 시간을 거의 두 배로 늘립니다.
Timothy Gonzalez

1
차종은 감지, 당신은 비동기 호출에 포장하는 새 작업을 만드는, 성능은 트레이드 오프입니다
Danilow

환상적으로 이것은 나에게도 효과가 있었으며 내 경우는 비동기 메서드를 호출하는 동기 메서드로 인해 발생했습니다. 감사합니다!
Leonardo Spina

4

허용 된 답변에 추가하기 위해 (댓글에 충분한 담당자가 아님) 이 예제와 같이 아래의 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);
}

반대표는 무엇입니까? 이 솔루션은 저에게 효과적입니다.
Cameron Jeffers 2015

Task이 완료 되기 전에 객체를 반환하고 반환 된 객체 의 변형이 실제로 발생하는시기를 결정하는 수단을 호출자에게 제공하지 않습니다.
Servy dec

흠, 알겠습니다. 그렇다면 수동으로 블로킹 while 루프 (또는 이와 유사한 것)를 사용하는 일종의 "작업이 완료 될 때까지 대기"메서드를 노출해야합니까? 아니면 그러한 블록을 GetFooSynchronous메서드에 넣을 까요?
Cameron Jeffers 2015

1
그렇게하면 교착 상태가됩니다. Task블로킹 대신 a 를 반환하여 완전히 비 동기화해야합니다 .
Servy dec

불행히도 이것은 옵션이 아닙니다. 클래스는 변경할 수없는 동기 인터페이스를 구현합니다.
Cameron Jeffers 2015

0

빠른 답변 :이 줄을 변경

ResultClass slowTotal = asyncTask.Result;

ResultClass slowTotal = await asyncTask;

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

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

ResultClass slowTotal = Task.Run(async ()=>await asyncTask).Result;
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.