HttpClient.GetAsync (…) nunca retorna ao usar wait / async

315

Edit: Esta pergunta parece que pode ser o mesmo problema, mas não tem respostas ...

Editar: No caso de teste 5, a tarefa parece estar parada no WaitingForActivationestado.

Eu encontrei um comportamento estranho usando o System.Net.Http.HttpClient no .NET 4.5 - onde "aguardando" o resultado de uma chamada para (por exemplo) httpClient.GetAsync(...)nunca retornará.

Isso ocorre apenas em determinadas circunstâncias ao usar a nova funcionalidade de idioma assíncrono / aguardado e a API de tarefas - o código sempre parece funcionar ao usar apenas continuações.

Aqui está um código que reproduz o problema - coloque-o em um novo "projeto MVC 4 WebApi" no Visual Studio 11 para expor os seguintes pontos de extremidade GET:

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

Cada um dos terminais aqui retorna os mesmos dados (os cabeçalhos de resposta de stackoverflow.com), exceto pelos /api/test5que nunca são concluídos.

Encontrei um bug na classe HttpClient ou estou usando a API de alguma forma?

Código a reproduzir:

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();
    }
}
Benjamin Fox
fonte
2
Não parece ser o mesmo problema, mas apenas para garantir que você saiba, existe um bug do MVC4 nos métodos assíncronos beta WRT que são concluídos de forma síncrona - consulte stackoverflow.com/questions/9627329/…
James Manning
Obrigado - eu vou cuidar disso. Nesse caso, acho que o método sempre deve ser assíncrono por causa da chamada para HttpClient.GetAsync(...)?
Benjamin Fox

Respostas:

468

Você está usando mal a API.

Aqui está a situação: no ASP.NET, apenas um thread pode manipular uma solicitação por vez. Você pode executar algum processamento paralelo, se necessário (emprestando encadeamentos adicionais do conjunto de encadeamentos), mas apenas um encadeamento teria o contexto de solicitação (os encadeamentos adicionais não terão o contexto de solicitação).

Isso é gerenciado pelo ASP.NETSynchronizationContext .

Por padrão, quando você awaita Task, o método continua em uma captura SynchronizationContext(ou uma captura TaskScheduler, se não houver SynchronizationContext). Normalmente, é exatamente isso que você deseja: uma ação do controlador assíncrona será awaitalgo e, quando retomada, retoma com o contexto da solicitação.

Então, aqui está o porquê test5falha:

  • Test5Controller.Geté executado AsyncAwait_GetSomeDataAsync(dentro do contexto de solicitação do ASP.NET).
  • AsyncAwait_GetSomeDataAsyncé executado HttpClient.GetAsync(dentro do contexto de solicitação do ASP.NET).
  • A solicitação HTTP é enviada e HttpClient.GetAsyncretorna um incompleto Task.
  • AsyncAwait_GetSomeDataAsyncaguarda o Task; como não está completo, AsyncAwait_GetSomeDataAsyncretorna um incompleto Task.
  • Test5Controller.Get bloqueia o encadeamento atual até que seja Taskconcluído.
  • A resposta HTTP é recebida e a Taskretornada por HttpClient.GetAsyncé concluída.
  • AsyncAwait_GetSomeDataAsynctenta retomar dentro do contexto de solicitação do ASP.NET. No entanto, já existe um segmento nesse contexto: o segmento bloqueado Test5Controller.Get.
  • Impasse.

Eis por que os outros funcionam:

  • ( test1,, test2e test3): Continuations_GetSomeDataAsyncagenda a continuação para o pool de threads, fora do contexto de solicitação do ASP.NET. Isso permite que o Taskretornado Continuations_GetSomeDataAsyncseja concluído sem ter que entrar novamente no contexto da solicitação.
  • ( test4E test6): Desde o Taské aguardado , o thread de solicitação ASP.NET não está bloqueado. Isso permite AsyncAwait_GetSomeDataAsyncusar o contexto de solicitação do ASP.NET quando estiver pronto para continuar.

E aqui estão as práticas recomendadas:

  1. Nos asyncmétodos da sua "biblioteca" , use ConfigureAwait(false)sempre que possível. No seu caso, isso mudaria AsyncAwait_GetSomeDataAsyncpara servar result = await httpClient.GetAsync("http://stackoverflow.com", HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false);
  2. Não bloqueie Tasks; está asynctodo o caminho. Em outras palavras, use em awaitvez de GetResult( Task.Resulte Task.Waittambém deve ser substituído por await).

Dessa forma, você obtém os dois benefícios: a continuação (o restante do AsyncAwait_GetSomeDataAsyncmétodo) é executada em um thread básico do pool de threads que não precisa entrar no contexto de solicitação do ASP.NET; e o próprio controlador é async(que não bloqueia um encadeamento de solicitação).

Mais Informações:

Atualização 13/07/2012: Incorporou esta resposta em uma postagem do blog .

Stephen Cleary
fonte
2
Existe alguma documentação para o ASP.NET SynchroniztaionContextque explica que pode haver apenas um thread no contexto para alguma solicitação? Se não, acho que deveria haver.
svick
8
Não está documentado em nenhum lugar do AFAIK.
Stephen Cleary
10
Obrigado - resposta incrível . A diferença de comportamento entre código (aparentemente) funcionalmente idêntico é frustrante, mas faz sentido com a sua explicação. Seria útil se a estrutura fosse capaz de detectar esses impasses e criar uma exceção em algum lugar.
Benjamin Fox
3
Existem situações em que o uso de .ConfigureAwait (false) em um contexto asp.net NÃO é recomendado? Parece-me que sempre deve ser usado e que é apenas em um contexto de interface do usuário que não deve ser usado, pois você precisa sincronizar com a interface do usuário. Ou estou perdendo o objetivo?
AlexGad
3
O ASP.NET SynchronizationContextfornece algumas funcionalidades importantes: ele flui o contexto da solicitação. Isso inclui todos os tipos de coisas, desde autenticação a cookies e cultura. Portanto, no ASP.NET, em vez de sincronizar novamente com a interface do usuário, você sincroniza novamente com o contexto da solicitação. Isso pode mudar em breve: o novo ApiControllertem um HttpRequestMessagecontexto como propriedade - portanto, pode não ser necessário fazer o fluxo fluir SynchronizationContext- mas ainda não sei.
Stephen Cleary
62

Editar: Geralmente, tente evitar o procedimento abaixo, exceto como um último esforço para evitar conflitos. Leia o primeiro comentário de Stephen Cleary.

Solução rápida a partir daqui . Em vez de escrever:

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

Experimentar:

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

Ou se você precisar de um resultado:

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

Na fonte (editada para corresponder ao exemplo acima):

O AsyncOperation agora será chamado no ThreadPool, onde não haverá um SynchronizationContext, e as continuações usadas dentro do AsyncOperation não serão forçadas de volta ao thread de chamada.

Para mim, isso parece uma opção utilizável, pois eu não tenho a opção de fazê-lo assíncrono o tempo todo (o que eu preferiria).

Da fonte:

Certifique-se de que a espera no método FooAsync não encontre um contexto para o qual empacotar. A maneira mais simples de fazer isso é invocar o trabalho assíncrono do ThreadPool, como agrupar a invocação em um Task.Run, por exemplo

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

O FooAsync agora será chamado no ThreadPool, onde não haverá um SynchronizationContext, e as continuações usadas dentro do FooAsync não serão forçadas de volta ao segmento que está chamando Sync ().

Ykok
fonte
7
Pode querer reler o seu link de origem; o autor recomenda não fazer isso. Funciona? Sim, mas apenas no sentido de evitar o conflito. Essa solução nega todos os benefícios do asynccódigo no ASP.NET e, de fato, pode causar problemas em grande escala. BTW, ConfigureAwaitnão "quebra o comportamento assíncrono apropriado" em nenhum cenário; é exatamente o que você deve usar no código da biblioteca.
precisa
2
É a primeira seção inteira, intitulada em negrito Avoid Exposing Synchronous Wrappers for Asynchronous Implementations. Todo o restante do post está explicando algumas maneiras diferentes de fazer isso, se você absolutamente precisar .
precisa
1
Adicionada a seção que encontrei na fonte - deixarei para os futuros leitores decidirem. Observe que você geralmente deve evitar fazer isso e fazê-lo apenas como último recurso (por exemplo, ao usar código assíncrono, você não tem controle).
Ykok
3
Eu gosto de todas as respostas aqui e, como sempre ... elas são todas baseadas no contexto (trocadilhos). Estou agrupando as chamadas assíncronas do HttpClient com uma versão síncrona, portanto não posso alterar esse código para adicionar o ConfigureAwait a essa biblioteca. Portanto, para evitar os impasses na produção, estou agrupando as chamadas Async em um Task.Run. Pelo que entendi, isso vai usar 1 thread extra por solicitação e evita o impasse. Suponho que, para ser totalmente compatível, preciso usar os métodos de sincronização do WebClient. Isso é muito trabalho para justificar, por isso precisarei de uma razão convincente para não seguir minha abordagem atual.
Samneric
1
Acabei criando um método de extensão para converter async em sincronização. Eu li aqui em algum lugar da mesma maneira que a estrutura .Net: public static TResult RunSync <TResult> (este Func <Tarefa <TResult>> func) {return _taskFactory .StartNew (func) .Unwrap () .GetAwaiter () .GetResult (); }
samneric
10

Como você está usando .Resultou .Waitou awaitisso acabará causando um impasse no seu código.

você pode usar ConfigureAwait(false)em asyncmétodos para evitar conflito

como isso:

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

você pode usar ConfigureAwait(false)sempre que possível para Não bloquear código assíncrono.

Hasan Fathi
fonte
2

Essas duas escolas não são realmente excludentes.

Aqui está o cenário em que você simplesmente precisa usar

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

ou algo parecido

   AsyncContext.Run(AsyncOperation);

Eu tenho uma ação MVC que está sob o atributo de transação do banco de dados. A idéia era (provavelmente) reverter tudo o que foi feito na ação, se algo der errado. Isso não permite a alternância de contexto; caso contrário, a reversão ou confirmação da transação falhará.

A biblioteca que eu preciso é assíncrona, pois é esperado que ela seja executada.

A única opção. Execute-o como uma chamada de sincronização normal.

Estou apenas dizendo a cada um o seu.

alex.peter
fonte
então você está sugerindo a primeira opção na sua resposta?
Don Cheadle
1

Vou colocar isso aqui mais por completude do que por relevância direta para o OP. Passei quase um dia depurando uma HttpClientsolicitação, me perguntando por que nunca recebi uma resposta.

Finalmente descobri que eu havia esquecido awaita asyncchamada mais abaixo na pilha de chamadas.

Parece tão bom quanto um ponto e vírgula.

Bondolin
fonte
-1

Estou procurando aqui:

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

E aqui:

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

E vendo:

Este tipo e seus membros devem ser usados ​​pelo compilador.

Considerando que a awaitversão funciona, e é a maneira 'certa' de fazer as coisas, você realmente precisa de uma resposta para esta pergunta?

Meu voto é: Uso indevido da API .

yamen
fonte
Eu não tinha notado isso, apesar de ter visto outro idioma em torno do qual indica que o uso da API GetResult () é um caso de uso suportado (e esperado).
27412 Benjamin Fox
1
Além disso, se você refatorar Test5Controller.Get()para eliminar o garçom com o seguinte: var task = AsyncAwait_GetSomeDataAsync(); return task.Result;O mesmo comportamento pode ser observado.
Benjamin Fox