Perché questa azione asincrona si blocca?


102

Ho un multi-tier .Net 4.5 applicazione che si chiama un metodo che utilizza C # 's nuova asynce awaitparole chiave che solo si blocca e non riesco a capire perché.

In fondo ho un metodo asincrono che estende la nostra utilità di database OurDBConn(fondamentalmente un wrapper per il sottostante DBConnectione gli DBCommandoggetti):

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;
}

Quindi ho un metodo asincrono di medio livello che lo chiama per ottenere alcuni totali a esecuzione lenta:

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

    return result;
}

Finalmente ho un metodo UI (un'azione MVC) che viene eseguito in modo sincrono:

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

// do other stuff that takes a few seconds

ResultClass slowTotal = asyncTask.Result;

Il problema è che rimane per sempre su quest'ultima riga. Fa la stessa cosa se chiamo asyncTask.Wait(). Se eseguo direttamente il metodo SQL lento, ci vogliono circa 4 secondi.

Il comportamento che mi aspetto è che quando arriva a asyncTask.Result, se non è finito, dovrebbe aspettare finché non lo è, e una volta che è dovrebbe restituire il risultato.

Se passo con un debugger, l'istruzione SQL viene completata e la funzione lambda termina, ma la return result;riga di GetTotalAsyncnon viene mai raggiunta.

Hai idea di cosa sto sbagliando?

Qualche suggerimento su dove devo indagare per risolvere questo problema?

Potrebbe essere un punto morto da qualche parte e, in tal caso, esiste un modo diretto per trovarlo?

Risposte:


150

Sì, questo è un punto morto, d'accordo. E un errore comune con il TPL, quindi non sentirti male.

Durante la scrittura await foo, il runtime, per impostazione predefinita, pianifica la continuazione della funzione sullo stesso SynchronizationContext su cui è stato avviato il metodo. In inglese, diciamo che hai chiamato il tuo ExecuteAsyncdal thread dell'interfaccia utente. La tua query viene eseguita sul thread del pool di thread (perché hai chiamato Task.Run), ma attendi il risultato. Ciò significa che il runtime pianificherà la " return result;" linea in modo che venga eseguito di nuovo sul thread dell'interfaccia utente, anziché pianificarla di nuovo nel pool di thread.

Allora come funziona questo deadlock? Immagina di avere solo questo codice:

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

Quindi la prima riga dà il via al lavoro asincrono. La seconda riga blocca quindi il thread dell'interfaccia utente . Quindi, quando il runtime vuole eseguire nuovamente la riga "return result" sul thread dell'interfaccia utente, non può farlo fino al Resultcompletamento. Ma ovviamente, il risultato non può essere dato fino a quando non avviene il ritorno. Deadlock.

Ciò illustra una regola chiave dell'utilizzo del TPL: quando si utilizza .Resultsu un thread dell'interfaccia utente (o un altro contesto di sincronizzazione di fantasia), è necessario fare attenzione a garantire che nulla da cui dipende Task sia pianificato per il thread dell'interfaccia utente. Oppure accade la malvagità.

Allora cosa fai? L'opzione n. 1 è utilizzare in attesa ovunque, ma come hai detto non è già un'opzione. La seconda opzione disponibile per te è semplicemente smettere di usare await. Puoi riscrivere le tue due funzioni per:

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"));
}

Qual è la differenza? Ora non c'è attesa da nessuna parte, quindi nulla viene pianificato in modo implicito nel thread dell'interfaccia utente. Per metodi semplici come questi che hanno un unico ritorno, non ha senso creare un var result = await...; return resultpattern " "; basta rimuovere il modificatore asincrono e passare direttamente l'oggetto attività. È meno sovraccarico, se non altro.

L'opzione n. 3 consiste nello specificare che non si desidera che le attese vengano nuovamente pianificate nel thread dell'interfaccia utente, ma solo pianificate nel pool di thread. Lo fai con il ConfigureAwaitmetodo, in questo modo:

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);
}

L'attesa di un'attività normalmente verrebbe pianificata nel thread dell'interfaccia utente, se ci sei tu; attendere il risultato di ContinueAwaitignorerà il contesto in cui ti trovi e pianificherà sempre il threadpool. Lo svantaggio di questo è che devi spargerlo ovunque in tutte le funzioni da cui dipende il tuo .Risult, perché qualsiasi mancato .ConfigureAwaitpotrebbe essere la causa di un altro deadlock.


6
BTW, la domanda riguarda ASP.NET, quindi non esiste un thread dell'interfaccia utente. Ma il problema con i deadlock è esattamente lo stesso, a causa di ASP.NET SynchronizationContext.
svick

Ciò ha spiegato molto, poiché avevo un codice .Net 4 simile che non presentava il problema ma che utilizzava il TPL senza le parole chiave async/ await.
Keith


Se qualcuno sta cercando il codice VB.net (come me) è spiegato qui: docs.microsoft.com/en-us/dotnet/visual-basic/programming-guide/…
MichaelDarkBlue


36

Questo è il classico asyncscenario di deadlock misto , come descrivo sul mio blog . Jason lo ha descritto bene: per impostazione predefinita, un "contesto" viene salvato in ogni awaite utilizzato per continuare il asyncmetodo. Questo "contesto" è la corrente a SynchronizationContextmeno che non lo sia null, nel qual caso è la corrente TaskScheduler. Quando il asyncmetodo tenta di continuare, prima rientra nel "contesto" acquisito (in questo caso, un ASP.NET SynchronizationContext). ASP.NET SynchronizationContextconsente solo un thread nel contesto alla volta e nel contesto è già presente un thread, il thread bloccato Task.Result.

Esistono due linee guida che eviteranno questo deadlock:

  1. Usa asyncfino in fondo. Hai detto che "non puoi" farlo, ma non so perché no. ASP.NET MVC su .NET 4.5 può certamente supportare le asyncazioni e non è una modifica difficile da apportare.
  2. Usa ConfigureAwait(continueOnCapturedContext: false)il più possibile. Ciò sovrascrive il comportamento predefinito di ripresa nel contesto acquisito.

Fa ConfigureAwait(false)garanzia che la funzione corrente riprende in un contesto differente?
chue x

Il framework MVC lo supporta, ma fa parte di un'app MVC esistente con molti JS lato client già presenti. Non posso passare facilmente a asyncun'azione senza interrompere il modo in cui funziona sul lato client. Certamente ho intenzione di indagare su questa opzione a lungo termine.
Keith

Giusto per chiarire il mio commento, ero curioso di sapere se utilizzare ConfigureAwait(false)l'albero delle chiamate avrebbe risolto il problema dell'OP.
chue x

3
@ Keith: l'esecuzione di un'azione MVC asyncnon influisce affatto sul lato client. Lo spiego in un altro post del blog, asyncnon cambia il protocollo HTTP .
Stephen Cleary

1
@ Keith: è normale asyncche "cresca" attraverso il codice base. Se il metodo del controller può dipendere da operazioni asincrone, il metodo della classe base dovrebbe restituire Task<ActionResult>. La transizione di un progetto di grandi dimensioni a asyncè sempre scomoda perché mescolare asynce sincronizzare il codice è difficile e complicato. Il asynccodice puro è molto più semplice.
Stephen Cleary

12

Ero nella stessa situazione di deadlock ma nel mio caso chiamando un metodo asincrono da un metodo di sincronizzazione, ciò che funziona per me era:

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

è un buon approccio, qualche idea?


Questa soluzione funziona anche per me, ma non sono sicuro che sia una buona soluzione o potrebbe non funzionare da qualche parte. Chiunque può spiegarlo
Konstantin Vdovkin

beh finalmente sono andato con questa soluzione e sta lavorando in un ambiente produttivo senza problemi .....
Danilow

1
Penso che tu stia subendo un calo di prestazioni usando Task.Run. Nel mio test Task.Run sta quasi raddoppiando il tempo di esecuzione per una richiesta http di 100 ms.
Timothy Gonzalez

1
ha senso, stai creando una nuova attività per avvolgere una chiamata asincrona, le prestazioni sono il compromesso
Danilow

Fantastico, questo ha funzionato anche per me, il mio caso è stato causato anche da un metodo sincrono che ne chiamava uno asincrono. Grazie!
Leonardo Spina

4

Solo per aggiungere alla risposta accettata (non abbastanza rappresentante per commentare), ho riscontrato questo problema durante il blocco dell'uso task.Result, anche se ogni awaitsotto aveva ConfigureAwait(false), come in questo esempio:

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);
}

Il problema in realtà risiedeva nel codice della libreria esterna. Il metodo della libreria asincrona ha cercato di continuare nel contesto di sincronizzazione della chiamata, indipendentemente da come ho configurato l'attesa, causando un deadlock.

Pertanto, la risposta è stata di eseguire il rollio della mia versione del codice della libreria esterna ExternalLibraryStringAsync, in modo che avesse le proprietà di continuazione desiderate.


risposta sbagliata per scopi storici

Dopo molto dolore e angoscia, ho trovato la soluzione sepolta in questo post del blog (Ctrl-f per "deadlock"). Ruota intorno all'uso task.ContinueWith, invece che al nudo task.Result.

Esempio di deadlock in precedenza:

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);
}

Evita il deadlock in questo modo:

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);
}

A cosa serve il downvote? Questa soluzione sta funzionando per me.
Cameron Jeffers

Restituisci l'oggetto prima che Tasksia stato completato e non fornisci al chiamante alcun mezzo per determinare quando si verifica effettivamente la mutazione dell'oggetto restituito.
Servizio

hmm si capisco. Quindi dovrei esporre una sorta di metodo di "attesa fino al completamento dell'attività" che utilizza un ciclo while di blocco manuale (o qualcosa del genere)? O impacchetta un blocco del genere nel GetFooSynchronousmetodo?
Cameron Jeffers

1
Se lo fai, si bloccherà. È necessario eseguire l'asincronizzazione fino in fondo restituendo a Taskinvece di bloccare.
Servizio

Purtroppo non è un'opzione, la classe implementa un'interfaccia sincrona che non posso modificare.
Cameron Jeffers

0

risposta rapida: cambia questa riga

ResultClass slowTotal = asyncTask.Result;

per

ResultClass slowTotal = await asyncTask;

perché? non dovresti usare .result per ottenere il risultato delle attività all'interno della maggior parte delle applicazioni tranne le applicazioni della console se lo fai il tuo programma si bloccherà quando arriva lì

puoi anche provare il codice seguente se vuoi usare .Result

ResultClass slowTotal = Task.Run(async ()=>await asyncTask).Result;
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.