HttpClient.GetAsync (...) non ritorna mai quando si utilizza waitit / async


315

Modifica: questa domanda sembra che potrebbe essere lo stesso problema, ma non ha risposte ...

Modifica: nel caso di test 5 l'attività sembra bloccata nello WaitingForActivationstato.

Ho riscontrato alcuni comportamenti strani utilizzando System.Net.Http.HttpClient in .NET 4.5 - in cui "in attesa" il risultato di una chiamata (ad es.) httpClient.GetAsync(...)Non tornerà mai più.

Ciò si verifica solo in determinate circostanze quando si utilizza la nuova funzionalità di linguaggio asincrono / wait e l'API Task: il codice sembra funzionare sempre quando si utilizzano solo continuazioni.

Ecco un po 'di codice che riproduce il problema: trascinalo in un nuovo "progetto MVC 4 WebApi" in Visual Studio 11 per esporre i seguenti endpoint GET:

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

Ciascuno degli endpoint qui restituisce gli stessi dati (le intestazioni di risposta da stackoverflow.com) ad eccezione del /api/test5quale non viene mai completato.

Ho riscontrato un bug nella classe HttpClient o utilizzo in qualche modo l'API?

Codice da riprodurre:

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
Non sembra essere lo stesso problema, ma solo per essere sicuri di conoscerlo, c'è un bug MVC4 nei metodi asincroni beta WRT che si completano in modo sincrono - vedi stackoverflow.com/questions/9627329/…
James Manning

Grazie - Starò attento. In questo caso penso che il metodo dovrebbe essere sempre asincrono a causa della chiamata a HttpClient.GetAsync(...)?
Benjamin Fox,

Risposte:


468

Stai abusando dell'API.

Ecco la situazione: in ASP.NET, solo un thread può gestire una richiesta alla volta. Se necessario, è possibile eseguire alcune elaborazioni parallele (prendendo in prestito thread aggiuntivi dal pool di thread), ma solo un thread avrebbe il contesto della richiesta (i thread aggiuntivi non hanno il contesto della richiesta).

Questo è gestito da ASP.NETSynchronizationContext .

Per impostazione predefinita, quando si awaita Task, il metodo riprende su un acquisito SynchronizationContext(o acquisito TaskScheduler, se non è presente SynchronizationContext). Normalmente, questo è proprio quello che vuoi: un'azione di controller asincrona farà awaitqualcosa e, quando riprende, riprende con il contesto della richiesta.

Quindi, ecco perché test5non riesce:

  • Test5Controller.Getviene eseguito AsyncAwait_GetSomeDataAsync(nel contesto della richiesta ASP.NET).
  • AsyncAwait_GetSomeDataAsyncviene eseguito HttpClient.GetAsync(nel contesto della richiesta ASP.NET).
  • La richiesta HTTP viene inviata e HttpClient.GetAsyncrestituisce un messaggio non completato Task.
  • AsyncAwait_GetSomeDataAsyncattende il Task; poiché non è completo, AsyncAwait_GetSomeDataAsyncrestituisce un non completato Task.
  • Test5Controller.Get blocca il thread corrente fino al Taskcompletamento.
  • La risposta HTTP arriva e il Taskreso HttpClient.GetAsyncviene completato.
  • AsyncAwait_GetSomeDataAsynctenta di riprendere nel contesto della richiesta ASP.NET. Tuttavia, esiste già un thread in quel contesto: il thread è bloccato Test5Controller.Get.
  • Deadlock.

Ecco perché gli altri funzionano:

  • ( test1,, test2e test3): Continuations_GetSomeDataAsyncpianifica la continuazione nel pool di thread, al di fuori del contesto della richiesta ASP.NET. Ciò consente al Taskreso Continuations_GetSomeDataAsyncdi completarsi senza dover rientrare nel contesto della richiesta.
  • ( test4e test6): poiché Taskè atteso , il thread di richiesta ASP.NET non è bloccato. Ciò consente AsyncAwait_GetSomeDataAsyncdi utilizzare il contesto della richiesta ASP.NET quando è pronto per continuare.

Ed ecco le migliori pratiche:

  1. Nei asyncmetodi "libreria" , utilizzare ConfigureAwait(false)quando possibile. Nel tuo caso, questo cambierebbe AsyncAwait_GetSomeDataAsyncdi esserevar result = await httpClient.GetAsync("http://stackoverflow.com", HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false);
  2. Non bloccare su Tasks; è asyncfino in fondo. In altre parole, utilizzare awaitinvece di GetResult( Task.Resulte Task.Waitdovrebbe essere sostituito anche con await).

In questo modo, si ottengono entrambi i vantaggi: la continuazione (il resto del AsyncAwait_GetSomeDataAsyncmetodo) viene eseguita su un thread di pool di thread di base che non deve entrare nel contesto della richiesta ASP.NET; e il controller stesso è async(che non blocca un thread di richiesta).

Maggiori informazioni:

Aggiornamento 13/07/2012: incorporata questa risposta in un post sul blog .


2
Esiste della documentazione per ASP.NET SynchroniztaionContextche spiega che può esserci un solo thread nel contesto per una richiesta? In caso contrario, penso che ci dovrebbe essere.
svick,

8
Non è documentato da nessuna parte AFAIK.
Stephen Cleary,

10
Grazie - risposta fantastica . La differenza di comportamento tra codice (apparentemente) funzionalmente identico è frustrante ma ha senso con la tua spiegazione. Sarebbe utile se il framework fosse in grado di rilevare tali deadlock e sollevare un'eccezione da qualche parte.
Benjamin Fox,

3
Ci sono situazioni in cui l'uso di .ConfigureAwait (false) in un contesto asp.net NON è raccomandato? Mi sembra che dovrebbe sempre essere usato e che è solo in un contesto di interfaccia utente che non dovrebbe essere utilizzato poiché è necessario sincronizzare con l'interfaccia utente. O mi manca il punto?
AlexGad,

3
ASP.NET SynchronizationContextfornisce alcune importanti funzionalità: scorre il contesto della richiesta. Ciò include tutti i tipi di cose dall'autenticazione ai cookie alla cultura. Quindi in ASP.NET, invece di sincronizzare nuovamente con l'interfaccia utente, si sincronizza nuovamente con il contesto della richiesta. Questo potrebbe cambiare a breve: il nuovo ApiControllerha un HttpRequestMessagecontesto come proprietà - quindi potrebbe non essere necessario scorrere il contesto SynchronizationContext- ma non lo so ancora.
Stephen Cleary,

62

Modifica: generalmente cerca di evitare di fare ciò che segue, tranne come ultimo tentativo di scappare per evitare deadlock. Leggi il primo commento di Stephen Cleary.

Soluzione rapida da qui . Invece di scrivere:

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

Provare:

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

O se hai bisogno di un risultato:

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

Dalla fonte (modificato per corrispondere all'esempio sopra):

AsyncOperation verrà ora richiamato su ThreadPool, dove non ci sarà un SynchronizationContext, e le continuazioni utilizzate all'interno di AsyncOperation non verranno forzate al thread invocante.

Per me questa sembra un'opzione utilizzabile poiché non ho la possibilità di renderla completamente asincrona (cosa che preferirei).

Dalla fonte:

Assicurarsi che l'attesa nel metodo FooAsync non trovi un contesto in cui eseguire il marshalling. Il modo più semplice per farlo è invocare il lavoro asincrono dal ThreadPool, ad esempio avvolgendo l'invocazione in un'attività.

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

FooAsync ora verrà richiamato su ThreadPool, dove non ci sarà un SynchronizationContext, e le continuazioni utilizzate all'interno di FooAsync non verranno forzate al thread che invoca Sync ().


7
Potrebbe voler rileggere il tuo link sorgente; l'autore raccomanda di non farlo. Funziona? Sì, ma solo nel senso che si evita lo stallo. Questa soluzione annulla tutti i vantaggi del asynccodice su ASP.NET e può infatti causare problemi su vasta scala. A proposito, ConfigureAwaitnon "rompe il comportamento asincrono" in nessuno scenario; è esattamente quello che dovresti usare nel codice della libreria.
Stephen Cleary,

2
È l'intera prima sezione, intitolata in grassetto Avoid Exposing Synchronous Wrappers for Asynchronous Implementations. L'intero resto del post sta spiegando alcuni modi diversi per farlo, se assolutamente necessario .
Stephen Cleary,

1
Aggiunta la sezione che ho trovato nella fonte: lascerò la decisione ai futuri lettori. Si noti che in genere si dovrebbe cercare di evitare di farlo e farlo solo come ultima risorsa (ad es. Quando si utilizza il codice asincrono non si ha il controllo).
Ykok,

3
Mi piacciono tutte le risposte qui e come sempre .... sono tutte basate sul contesto (gioco di parole voluto lol). Sto avvolgendo le chiamate Async di HttpClient con una versione sincrona, quindi non posso cambiare quel codice per aggiungere ConfigureAwait a quella libreria. Quindi, per evitare i deadlock nella produzione, sto avvolgendo le chiamate Async in un Task.Run. Quindi, come ho capito, questo utilizzerà 1 thread aggiuntivo per richiesta ed evita il deadlock. Presumo che per essere completamente conforme, sia necessario utilizzare i metodi di sincronizzazione di WebClient. Questo è molto lavoro da giustificare, quindi avrò bisogno di un motivo convincente che non si attenga al mio approccio attuale.
samneric

1
Ho finito per creare un metodo di estensione per convertire Async in Sync. Ho letto qui da qualche parte allo stesso modo del framework .Net: public statico TResult RunSync <TResult> (questo Func <Task <TResult>> func) {return _taskFactory .StartNew (func) .Unwrap () .GetAwaiter () .GetResult (); }
samneric

10

Dal momento che stai usando .Resulto .Waito awaitquesto finirà per causare un deadlock nel tuo codice.

è possibile utilizzare ConfigureAwait(false)nei asyncmetodi per prevenire deadlock

come questo:

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

è possibile utilizzare ConfigureAwait(false)ovunque sia possibile per Non bloccare codice asincrono.


2

Queste due scuole non stanno davvero escludendo.

Ecco lo scenario in cui devi semplicemente usare

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

o qualcosa del genere

   AsyncContext.Run(AsyncOperation);

Ho un'azione MVC che è sotto l'attributo di transazione del database. L'idea era (probabilmente) di ripristinare tutto ciò che è stato fatto nell'azione se qualcosa va storto. Ciò non consente il cambio di contesto, altrimenti il ​​rollback o il commit della transazione falliranno.

La libreria di cui ho bisogno è asincrona in quanto è prevista l'esecuzione asincrona.

L'unica opzione. Eseguilo come una normale chiamata di sincronizzazione.

Sto solo dicendo a ciascuno il suo.


quindi stai suggerendo la prima opzione nella tua risposta?
Don Cheadle,

1

Lo inserirò qui più per completezza che per rilevanza diretta per il PO. Ho trascorso quasi un giorno a eseguire il debug di una HttpClientrichiesta, chiedendomi perché non avrei mai ricevuto risposta.

Alla fine ho scoperto che avevo dimenticato awaitla asyncchiamata più in basso nello stack di chiamate.

Sembra buono come perdere un punto e virgola.


-1

Sto guardando qui:

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

E qui:

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

E vedendo:

Questo tipo e i suoi membri sono destinati all'uso da parte del compilatore.

Considerando che la awaitversione funziona ed è il modo "giusto" di fare le cose, hai davvero bisogno di una risposta a questa domanda?

Il mio voto è: uso improprio dell'API .


Non l'avevo notato, anche se ho visto un altro linguaggio intorno al quale indica che l'utilizzo dell'API GetResult () è un caso d'uso supportato (e previsto).
Benjamin Fox,

1
Inoltre, se si refactoring Test5Controller.Get()per eliminare il cameriere con il seguente: var task = AsyncAwait_GetSomeDataAsync(); return task.Result;Lo stesso comportamento può essere osservato.
Benjamin Fox,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.