Perché questa azione asincrona si blocca?

Ho un’applicazione multi-livello .Net 4.5 che chiama un metodo usando il nuovo async C # e await parole chiave che si bloccano e non vedo perché.

In fondo ho un metodo asincrono che estende la nostra utilità di database OurDBConn (fondamentalmente un wrapper per gli oggetti DBConnection e DBCommand sottostanti):

 public static async Task ExecuteAsync(this OurDBConn dataSource, Func 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; } 

Poi ho un metodo asincrono di medio livello che chiama questo per ottenere alcuni totali di esecuzione lenta:

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

Finalmente ho un metodo UI (un’azione MVC) che gira in modo sincrono:

 Task asyncTask = midLevelClass.GetTotalAsync(...); // do other stuff that takes a few seconds ResultClass slowTotal = asyncTask.Result; 

Il problema è che si blocca su quell’ultima riga per sempre. Fa la stessa cosa se chiamo asyncTask.Wait() . Se eseguo direttamente il metodo SQL lento, sono necessari 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 il return result; linea di GetTotalAsync non viene mai raggiunta.

Qualche idea su cosa sto facendo male?

Qualche suggerimento su dove devo indagare per sistemare questo?

Potrebbe essere una situazione di stallo da qualche parte, e se è così esiste un modo diretto per trovarlo?

Sì, questo è un punto morto. E un errore comune con la TPL, quindi non sentirti male.

Quando si scrive await foo , il runtime, per impostazione predefinita, pianifica la continuazione della funzione sullo stesso SynchronizationContext su cui è stato avviato il metodo. In inglese, supponiamo che tu abbia chiamato ExecuteAsync dal thread dell’interfaccia utente. La tua query viene eseguita sul thread del Task.Run (perché hai chiamato Task.Run ), ma poi Task.Run il risultato. Ciò significa che il runtime pianificherà la riga ” return result; ” per eseguire nuovamente il thread dell’interfaccia utente, anziché pianificarlo di nuovo sul threadpool.

Quindi, 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 . Pertanto, quando il runtime desidera eseguire la riga “risultato risultato” sul thread dell’interfaccia utente, non può farlo finché non viene completato il Result . Ma ovviamente, il risultato non può essere dato fino a quando non si verifica il ritorno. Deadlock.

Questo illustra una regola chiave dell’utilizzo di TPL: quando si utilizza .Result su un thread dell’interfaccia utente (o qualche altro contesto di sincronizzazione fantasia), è necessario fare attenzione a garantire che nulla del task da cui dipende dipende dal thread dell’interfaccia utente. Oppure succede la malvagità.

Allora cosa fai? L’opzione numero 1 è valida ovunque, ma come hai detto non è già un’opzione. La seconda opzione che è disponibile per te è semplicemente smettere di usare Attendere. Puoi riscrivere le tue due funzioni per:

 public static Task ExecuteAsync(this OurDBConn dataSource, Func 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 GetTotalAsync( ... ) { return this.DBConnection.ExecuteAsync( ds => ds.Execute("select slow running data into result")); } 

Qual è la differenza? Ora non c’è da aspettare da nessuna parte, quindi niente è programmato implicitamente nel thread dell’interfaccia utente. Per metodi semplici come questi che hanno un singolo ritorno, non ha senso fare un pattern ” var result = await...; return result “; basta rimuovere il modificatore asincrono e passare direttamente l’object compito. È meno spese generali, se non altro.

L’opzione n. 3 specifica che non vuoi che le tue attese pianificino il thread dell’interfaccia utente, ma pianifichi semplicemente il thread dell’interfaccia utente. Lo fai con il metodo ConfigureAwait , in questo modo:

 public static async Task GetTotalAsync( ... ) { var resultTask = this.DBConnection.ExecuteAsync( ds => return ds.Execute("select slow running data into result"); return await resultTask.ConfigureAwait(false); } 

In attesa di un’attività normalmente si pianificherà il thread dell’interfaccia utente se ci si trova su di esso; in attesa del risultato di ContinueAwait ignorerà il contesto in cui ti trovi e pianificherà sempre il threadpool. Il rovescio della medaglia di questo è che devi spargere questo ovunque in tutte le funzioni tue. Il risultato dipende, perché ogni .ConfigureAwait potrebbe essere la causa di un altro deadlock.

Questo è il classico scenario di deadlock misto- async , come descrivo sul mio blog . Jason lo ha descritto bene: per impostazione predefinita, un “contesto” viene salvato ad ogni await e utilizzato per continuare il metodo async . Questo “contesto” è l’attuale SynchronizationContext meno che non sia null , nel qual caso è l’attuale TaskScheduler . Quando il metodo async tenta di continuare, per prima cosa rientra nel “contesto” acquisito (in questo caso, un SynchronizationContext ASP.NET). ASP.NET SynchronizationContext consente un solo thread nel contesto alla volta e nel contesto esiste già un thread, ovvero il thread bloccato su Task.Result .

Ci sono due linee guida che eviteranno questo stallo:

  1. Utilizzare async fino in fondo. Dici che non puoi “farlo”, ma non sono sicuro perché no. ASP.NET MVC su .NET 4.5 può certamente supportare azioni async , e non è un cambiamento difficile da fare.
  2. Usa ConfigureAwait(continueOnCapturedContext: false) il più ansible. Questo sovrascrive il comportamento predefinito di riprendere nel contesto catturato.

Ero nella stessa situazione di stallo, ma nel mio caso chiamare un metodo asincrono da un metodo di sincronizzazione, ciò che funziona per me è stato:

 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?

Giusto per aggiungere alla risposta accettata (non abbastanza rep per commentare), ho avuto questo problema quando task.Result usando task.Result , evento sebbene ogni await sotto di essa avesse 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 GetInfoAsync() { return await ExternalLibraryStringAsync().ConfigureAwait(false); } 

Il problema si trova in realtà con il codice della libreria esterna. Il metodo della libreria asincrona ha cercato di continuare nel contesto di sincronizzazione chiamante, indipendentemente da come ho configurato l’attesa, causando il deadlock.

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


risposta sbagliata per scopi storici

Dopo tanto dolore e angoscia, ho trovato la soluzione sepolta in questo post del blog (Ctrl-f per “deadlock”). Riguarda l’uso di task.ContinueWith , invece della nuda task.Result .

Esempio di deadlocking precedente:

 public Foo GetFooSynchronous() { var foo = new Foo(); foo.Info = GetInfoAsync.Result; // often deadlocks in ASP.NET return foo; } private async Task GetInfoAsync() { return await ExternalLibraryStringAsync().ConfigureAwait(false); } 

Evita lo stallo 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 GetInfoAsync { return await ExternalLibraryStringAsync().ConfigureAwait(false); }