대기 / 비동기 사용시 HttpClient.GetAsync (…)가 반환되지 않음


315

편집 : 이 질문 은 같은 문제 일 것 같지만 응답이 없습니다 ...

편집 : 테스트 사례 5에서 작업이 정지 된 것처럼 보입니다 WaitingForActivation.

.NET 4.5에서 System.Net.Http.HttpClient를 사용하여 이상한 동작이 발생했습니다. 여기서 (예를 들어) 호출의 결과를 "대기"하면 httpClient.GetAsync(...)반환되지 않습니다.

이는 새로운 비동기 / 대기 언어 기능 및 작업 API를 사용할 때 특정 상황에서만 발생합니다. 코드는 계속 사용하는 경우 항상 작동하는 것 같습니다.

다음은 문제를 재현하는 코드입니다. Visual Studio 11의 새로운 "MVC 4 WebApi 프로젝트"에 다음 GET 엔드 ​​포인트를 표시하십시오.

/api/test1
/api/test2
/api/test3
/api/test4
/api/test5 <--- never completes
/api/test6

여기서 각 끝점 /api/test5은 완료되지 않은 것을 제외하고는 동일한 데이터 (stackoverflow.com의 응답 헤더)를 반환합니다 .

HttpClient 클래스에서 버그가 발생 했습니까, 아니면 어떤 식 으로든 API를 잘못 사용하고 있습니까?

재현 할 코드 :

public class BaseApiController : ApiController
{
    /// <summary>
    /// Retrieves data using continuations
    /// </summary>
    protected Task<string> Continuations_GetSomeDataAsync()
    {
        var httpClient = new HttpClient();

        var t = httpClient.GetAsync("http://stackoverflow.com", HttpCompletionOption.ResponseHeadersRead);

        return t.ContinueWith(t1 => t1.Result.Content.Headers.ToString());
    }

    /// <summary>
    /// Retrieves data using async/await
    /// </summary>
    protected async Task<string> AsyncAwait_GetSomeDataAsync()
    {
        var httpClient = new HttpClient();

        var result = await httpClient.GetAsync("http://stackoverflow.com", HttpCompletionOption.ResponseHeadersRead);

        return result.Content.Headers.ToString();
    }
}

public class Test1Controller : BaseApiController
{
    /// <summary>
    /// Handles task using Async/Await
    /// </summary>
    public async Task<string> Get()
    {
        var data = await Continuations_GetSomeDataAsync();

        return data;
    }
}

public class Test2Controller : BaseApiController
{
    /// <summary>
    /// Handles task by blocking the thread until the task completes
    /// </summary>
    public string Get()
    {
        var task = Continuations_GetSomeDataAsync();

        var data = task.GetAwaiter().GetResult();

        return data;
    }
}

public class Test3Controller : BaseApiController
{
    /// <summary>
    /// Passes the task back to the controller host
    /// </summary>
    public Task<string> Get()
    {
        return Continuations_GetSomeDataAsync();
    }
}

public class Test4Controller : BaseApiController
{
    /// <summary>
    /// Handles task using Async/Await
    /// </summary>
    public async Task<string> Get()
    {
        var data = await AsyncAwait_GetSomeDataAsync();

        return data;
    }
}

public class Test5Controller : BaseApiController
{
    /// <summary>
    /// Handles task by blocking the thread until the task completes
    /// </summary>
    public string Get()
    {
        var task = AsyncAwait_GetSomeDataAsync();

        var data = task.GetAwaiter().GetResult();

        return data;
    }
}

public class Test6Controller : BaseApiController
{
    /// <summary>
    /// Passes the task back to the controller host
    /// </summary>
    public Task<string> Get()
    {
        return AsyncAwait_GetSomeDataAsync();
    }
}

2
참조 - 그것은 동일한 문제가 나타나지 않지만, 단지 당신이 그것에 대해 알 수 있도록, 동 기적으로 완료하는 베타 WRT 비동기 방식의 MVC4 버그있다 stackoverflow.com/questions/9627329/...
제임스 매닝

고마워-조심 할게 이 경우 메소드 호출은 항상 비동기식이어야한다고 생각 HttpClient.GetAsync(...)합니까?
Benjamin Fox

답변:


468

API를 잘못 사용하고 있습니다.

상황은 다음과 같습니다. ASP.NET에서는 한 번에 하나의 스레드 만 요청을 처리 할 수 ​​있습니다. 필요한 경우 병렬 처리를 수행 할 수 있지만 (스레드 풀에서 추가 스레드를 차용) 하나의 스레드 만 요청 컨텍스트를 갖습니다 (추가 스레드에는 요청 컨텍스트가 없음).

이것은 ASP.NET에 의해 관리됩니다SynchronizationContext .

기본적으로 awaita Task이면 메서드는 캡처 된 SynchronizationContext(또는 TaskScheduler없는 경우 캡처 된) 에서 다시 시작됩니다 SynchronizationContext. 일반적으로 이것은 원하는 것입니다. 비동기 컨트롤러 작업은 await무언가를하고 다시 시작하면 요청 컨텍스트와 함께 다시 시작됩니다.

따라서 test5실패하는 이유 는 다음과 같습니다.

  • Test5Controller.GetAsyncAwait_GetSomeDataAsyncASP.NET 요청 컨텍스트 내에서 실행합니다 .
  • AsyncAwait_GetSomeDataAsyncHttpClient.GetAsyncASP.NET 요청 컨텍스트 내에서 실행합니다 .
  • HTTP 요청이 전송되고 HttpClient.GetAsync미완료를 반환합니다 Task.
  • AsyncAwait_GetSomeDataAsync기다립니다 Task; 완료되지 않았으므로 미완료를 AsyncAwait_GetSomeDataAsync반환합니다 Task.
  • Test5Controller.Get Task완료 될 때까지 현재 스레드를 차단 합니다 .
  • HTTP 응답이 들어오고로 Task리턴됩니다 HttpClient.GetAsync.
  • AsyncAwait_GetSomeDataAsyncASP.NET 요청 컨텍스트 내에서 재개를 시도합니다. 그러나 해당 컨텍스트에는 이미 스레드가 Test5Controller.Get있습니다. 스레드가에 차단되었습니다 .
  • 이중 자물쇠.

다른 것들이 작동하는 이유는 다음과 같습니다.

  • ( test1,, test2test3) : ASP.NET 요청 컨텍스트 외부Continuations_GetSomeDataAsync스레드 풀로 연속을 예약합니다 . 이를 통해 요청 컨텍스트를 다시 입력하지 않고도 리턴 된 사용자 가 완료 될 수 있습니다 .TaskContinuations_GetSomeDataAsync
  • ( test4and test6) : Task기다리고 있기 때문에 ASP.NET 요청 스레드는 차단되지 않습니다. 이를 통해 AsyncAwait_GetSomeDataAsyncASP.NET 요청 컨텍스트를 계속 사용할 수있게 됩니다.

모범 사례는 다음과 같습니다.

  1. "라이브러리" async방법에서 ConfigureAwait(false)가능할 때마다 사용 하십시오. 귀하의 경우이 변경 AsyncAwait_GetSomeDataAsync될 것입니다var result = await httpClient.GetAsync("http://stackoverflow.com", HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false);
  2. Tasks를 막지 마십시오 . 그건 async모든 방법을 아래로. 다시 말해, await대신 GetResult( Task.ResultTask.Wait사용해야합니다 await).

이렇게하면 두 가지 이점이 있습니다. 연속 ( AsyncAwait_GetSomeDataAsync메서드 의 나머지 )은 ASP.NET 요청 컨텍스트를 입력 할 필요가없는 기본 스레드 풀 스레드에서 실행됩니다. 컨트롤러 자체는 async(요청 스레드를 차단하지 않습니다).

추가 정보:

2012-07-13 업데이트 : 이 답변 을 블로그 게시물에 통합 했습니다 .


2
SynchroniztaionContext일부 요청에 대해 컨텍스트에 스레드가 하나만있을 수 있다고 설명하는 ASP.NET에 대한 문서가 있습니까? 그렇지 않다면 있어야한다고 생각합니다.
svick

8
AFAIK에는 문서화되어 있지 않습니다.
Stephen Cleary

10
감사합니다- 멋진 응답. 기능적으로 동일한 코드 (명백하게)의 동작 차이는 실망 스럽지만 설명에는 적합합니다. 프레임 워크가 이러한 교착 상태를 감지하고 어딘가에서 예외를 발생시킬 수 있다면 유용 할 것입니다.
Benjamin Fox

3
asp.net 컨텍스트에서 .ConfigureAwait (false)를 사용하지 않는 상황이 있습니까? 항상 사용해야하고 UI 컨텍스트에서만 UI와 동기화해야하므로 사용해서는 안되는 것처럼 보입니다. 아니면 요점을 놓치고 있습니까?
AlexGad

3
ASP.NET SynchronizationContext은 몇 가지 중요한 기능을 제공합니다. 요청 컨텍스트를 전달합니다. 여기에는 인증에서 쿠키, 문화에 이르는 모든 종류의 것들이 포함됩니다. 따라서 ASP.NET에서는 UI로 다시 동기화하는 대신 요청 컨텍스트로 다시 동기화합니다. 새로운 :이 곧 변경 될 수 있습니다 ApiController가지고있다 HttpRequestMessage가 그래서 - 속성으로 컨텍스트를 할 수 있습니다 를 통해 문맥 흐름 할 필요가 없습니다 SynchronizationContext-하지만 난 아직 모른다.
Stephen Cleary

61

편집 : 일반적으로 교착 상태를 피하기위한 마지막 도랑 노력을 제외하고는 아래 작업을 피하십시오. Stephen Cleary의 첫 번째 의견을 읽으십시오.

여기 에서 빠른 수정 . 글을 쓰는 대신 :

Task tsk = AsyncOperation();
tsk.Wait();

시험:

Task.Run(() => AsyncOperation()).Wait();

또는 결과가 필요한 경우 :

var result = Task.Run(() => AsyncOperation()).Result;

소스에서 (위 예제와 일치하도록 편집) :

ThreadPool에서 AsyncOperation이 호출되어 SynchronizationContext가없고 AsyncOperation 내부에서 사용 된 연속은 호출 스레드로 다시 강제되지 않습니다.

나를 위해 이것은 비동기식으로 만들 수있는 옵션이 없기 때문에 사용 가능한 옵션처럼 보입니다.

출처에서 :

FooAsync 메소드의 대기가 마샬링 할 컨텍스트를 찾지 않는지 확인하십시오. 이를 수행하는 가장 간단한 방법은 ThreadPool에서 비동기 작업을 호출하는 것입니다.

int Sync () {return Task.Run (() => Library.FooAsync ()). Result; }

이제 SynchronizationContext가없는 ThreadPool에서 FooAsync가 호출되고 FooAsync 내부에서 사용 된 연속은 Sync ()를 호출하는 스레드로 다시 강제되지 않습니다.


7
소스 링크를 다시 읽고 싶을 수도 있습니다. 저자는 이것을 하지 않는 것이 좋습니다 . 작동합니까? 예, 그러나 교착 상태를 피한다는 의미에서만 가능합니다. 이 솔루션 은 ASP.NET에서 코드의 모든 이점을 무시 하고async 실제로 규모에 문제를 일으킬 수 있습니다. BTW ConfigureAwait는 어떤 시나리오에서도 "적절한 비동기 동작을 중단"하지 않습니다. 라이브러리 코드에서 사용해야하는 것과 정확히 같습니다.
Stephen Cleary

2
굵은 글씨로 제목이 지정된 첫 번째 섹션 전체 Avoid Exposing Synchronous Wrappers for Asynchronous Implementations입니다. 게시물의 전체 나머지는 그것을 할 수있는 몇 가지 방법을 설명되는 경우에 당신이 절대적으로 필요 로한다.
Stephen Cleary

1
소스에서 찾은 섹션을 추가했습니다. 나는 미래 독자들에게 결정을 내릴 것입니다. 일반적으로이 작업을 피하고 마지막 도랑 수단으로 만 수행해야합니다 (예 : 비동기 코드를 사용하는 경우 제어 할 수 없음).
Ykok December

3
나는 여기에 항상 모든 대답을 좋아합니다 .... 그들은 모두 문맥을 기반으로합니다 (pun 예정된 lol). HttpClient의 Async 호출을 동기 버전으로 래핑하므로 해당 라이브러리에 ConfigureAwait를 추가하기 위해 해당 코드를 변경할 수 없습니다. 따라서 프로덕션에서 교착 상태를 방지하기 위해 Async 호출을 Task.Run에 래핑합니다. 내가 알기로, 이것은 요청 당 1 개의 추가 스레드를 사용하고 교착 상태를 피할 것입니다. 나는 완전히 호환된다고 가정하고 WebClient의 동기화 방법을 사용해야합니다. 그것은 정당화하기 위해 많은 노력을 기울이고 있으므로 현재 접근 방식을 고수하지 않는 강력한 이유가 필요합니다.
samneric

1
비동기를 동기화로 변환하는 확장 방법을 만들었습니다. .Net 프레임 워크와 동일한 방식으로 여기를 읽었습니다. public static TResult RunSync <TResult> (this Func <Task <TResult >> func) {return _taskFactory .StartNew (func) .Unwrap () .GetAwaiter () .GetResult (); }
samneric

10

당신이 사용하고 있기 때문에 .Result.Wait또는 await이는 원인이 종료됩니다 교착 상태를 코드에서.

당신이 사용할 수 ConfigureAwait(false)async대한 방법 교착 상태를 방지

이처럼 :

var result = await httpClient.GetAsync("http://stackoverflow.com", HttpCompletionOption.ResponseHeadersRead)
                             .ConfigureAwait(false);

ConfigureAwait(false)비동기 코드 차단 안함에 대해 가능 하면 사용할 수 있습니다 .


2

이 두 학교는 실제로 제외되지 않습니다.

다음은 단순히 사용해야하는 시나리오입니다

   Task.Run(() => AsyncOperation()).Wait(); 

또는 같은

   AsyncContext.Run(AsyncOperation);

데이터베이스 트랜잭션 속성 아래에있는 MVC 작업이 있습니다. 아이디어는 문제가 발생했을 때 작업에서 수행 된 모든 것을 롤백하는 것입니다. 컨텍스트 전환이 허용되지 않으면 트랜잭션 롤백 또는 커밋이 실패합니다.

필요한 라이브러리는 비동기로 실행될 것으로 예상되므로 비동기입니다.

유일한 옵션입니다. 일반 동기화 호출로 실행하십시오.

나는 단지 그들 각자에게 말하고 있습니다.


그래서 당신은 당신의 대답에서 첫 번째 옵션을 제안하고 있습니까?
Don Cheadle

1

나는 OP와의 직접적인 관련성보다 완전성을 위해 여기에 더 많이 넣을 것입니다. 나는 HttpClient응답을 다시 얻지 못한 이유를 궁금해 하면서 거의 하루 동안 요청을 디버깅했습니다 .

마지막으로 I가 잊고 있었던 것을 발견 호출 스택 아래로 더 호출.awaitasync

세미콜론이없는 것 같은 느낌이 듭니다.


-1

나는 여기에서 찾고있다 :

http://msdn.microsoft.com/en-us/library/system.runtime.compilerservices.taskawaiter(v=vs.110).aspx

그리고 여기:

http://msdn.microsoft.com/en-us/library/system.runtime.compilerservices.taskawaiter.getresult(v=vs.110).aspx

그리고보고 :

이 유형과 해당 멤버는 컴파일러에서 사용하도록 설계되었습니다.

고려 await버전 일을하고, 일을의 권리 '방법이다, 당신은 정말이 질문에 대한 답변을해야합니까?

내 투표는 : API 오용 .


나는 다른 언어를 보았지만 GetResult () API를 사용하는 것이 지원되는 (그리고 예상되는) 유스 케이스임을 나타내는 다른 언어를 보았지만 알지 못했습니다.
Benjamin Fox

1
또한 Test5Controller.Get()다음과 같이 대기자를 제거하기 위해 리팩터링 하면 var task = AsyncAwait_GetSomeDataAsync(); return task.Result;동일한 동작이 관찰 될 수 있습니다.
Benjamin Fox
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.