¿Por qué cuelga esta acción asíncrona?

Tengo una aplicación .Net 4.5 de varios niveles que llama a un método que usa la nueva async C # y await palabras clave que simplemente se cuelgan y no puedo ver por qué.

En la parte inferior, tengo un método asíncrono que extiende nuestra utilidad de base de datos OurDBConn (básicamente un contenedor para los DBConnection subyacentes DBConnection y DBCommand ):

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

Luego tengo un método asincrónico de nivel medio que llama a esto para obtener algunos totales de ejecución lenta:

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

Finalmente tengo un método UI (una acción MVC) que se ejecuta sincrónicamente:

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

El problema es que se cuelga en la última línea para siempre. Hace lo mismo si llamo asyncTask.Wait() . Si ejecuto el método SQL lento directamente, demora unos 4 segundos.

El comportamiento que espero es que cuando llegue a asyncTask.Result , si no está terminado, debería esperar hasta que lo esté, y una vez que esté, debería devolver el resultado.

Si paso con un depurador, la instrucción SQL se completa y la función lambda finaliza, pero el return result; la return result; línea de GetTotalAsync nunca se alcanza.

¿Alguna idea de lo que estoy haciendo mal?

¿Alguna sugerencia sobre dónde debo investigar para solucionar esto?

¿Podría ser un punto muerto en algún lugar, y si es así hay alguna manera directa de encontrarlo?

Sí, eso es un punto muerto, de acuerdo. Y un error común con el TPL, así que no te sientas mal.

Cuando escribe await foo , el tiempo de ejecución, por defecto, progtwig la continuación de la función en el mismo SynchronizationContext en el que se inició el método. En inglés, digamos que ExecuteAsync a ExecuteAsync desde el hilo de UI. Su consulta se ejecuta en el hilo de subprocesos (porque llamó a Task.Run ), pero luego espera el resultado. Esto significa que el tiempo de ejecución progtwigrá su línea de ” return result; ” para ejecutar en el hilo de la interfaz de usuario, en lugar de volver a progtwigrla en el grupo de temas.

Entonces, ¿cómo funciona este punto muerto? Imagina que tienes este código:

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

Entonces la primera línea inicia el trabajo asincrónico. La segunda línea luego bloquea el hilo de UI . Entonces, cuando el tiempo de ejecución quiere ejecutar la línea “devolver resultados” en el hilo de la interfaz de usuario, no puede hacer eso hasta que el Result finalice. Pero, por supuesto, el resultado no puede darse hasta que el retorno ocurra. Punto muerto.

Esto ilustra una regla clave para usar el TPL: cuando usa .Result en un hilo de interfaz de usuario (u otro contexto de sincronización elegante), debe tener cuidado de asegurarse de que nada de lo que depende la tarea esté progtwigdo en el hilo de la interfaz de usuario. O bien, la maldad sucede.

Entonces, ¿Qué haces? La opción n. ° 1 es usar en todas partes, pero como dijiste, eso ya no es una opción. La segunda opción que está disponible para usted es simplemente dejar de usar await. Puedes reescribir tus dos funciones para:

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

¿Cual es la diferencia? Ahora no hay nada esperando, por lo que no hay nada que esté implícitamente progtwigdo en el hilo de la interfaz de usuario. Para métodos simples como estos que tienen un retorno único, no tiene sentido hacer un patrón ” var result = await...; return result “; simplemente elimine el modificador async y pase el objeto de tarea directamente. Es menos sobrecarga, si nada más.

La opción n. ° 3 es especificar que no desea que su espera vuelva a progtwigrse en la secuencia de la interfaz de usuario, sino que simplemente programe la secuencia de la interfaz de usuario. Haga esto con el método ConfigureAwait , así:

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

En espera de una tarea normalmente progtwigría el hilo de la interfaz de usuario si estás en ella; a la espera del resultado de ContinueAwait ignorará el contexto en el que se encuentre y siempre se progtwigrá en el grupo de subprocesos. La desventaja de esto es que tienes que rociar esto en todas las funciones en las que depende tu .Result, porque cualquier .ConfigureAwait perdido puede ser la causa de otro punto muerto.

Este es el clásico escenario de interlocking async mixto, como describo en mi blog . Jason lo describió bien: de forma predeterminada, se guarda un “contexto” en cada await y se usa para continuar el método async . Este “contexto” es el SynchronizationContext actual a menos que sea null , en cuyo caso es el TaskScheduler actual. Cuando el método async intenta continuar, primero vuelve a entrar en el “contexto” capturado (en este caso, un ASP.NET SynchronizationContext ). ASP.NET SynchronizationContext solo permite un hilo en el contexto a la vez, y ya hay un hilo en el contexto: el hilo bloqueado en Task.Result .

Hay dos pautas que evitarán este punto muerto:

  1. Use async todo el camino hacia abajo. Mencionas que “no puedes” hacer esto, pero no estoy seguro de por qué no. ASP.NET MVC en .NET 4.5 ciertamente puede soportar acciones async , y no es un cambio difícil de realizar.
  2. Use ConfigureAwait(continueOnCapturedContext: false) tanto como sea posible. Esto anula el comportamiento predeterminado de reanudar en el contexto capturado.

Estaba en la misma situación de punto muerto, pero en mi caso llamando a un método asíncrono desde un método de sincronización, lo que funciona para mí fue:

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

¿Es este un buen enfoque, alguna idea?

Solo para agregar a la respuesta aceptada (no hay suficientes representantes para comentar), tuve este problema cuando bloqueé el uso de task.Result , event though every task.Result below tenía ConfigureAwait(false) , como en este ejemplo:

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

En realidad, el problema radica en el código de la biblioteca externa. El método de la biblioteca asíncrono intentó continuar en el contexto de sincronización de llamadas, sin importar cómo configuré la espera, lo que condujo a un punto muerto.

Por lo tanto, la respuesta fue rodar mi propia versión del código de la biblioteca externa ExternalLibraryStringAsync , de modo que tuviera las propiedades de continuación deseadas.


respuesta incorrecta para propósitos históricos

Después de mucho dolor y angustia, encontré la solución enterrada en esta publicación de blog (Ctrl-f para ‘interlocking’). Se trata de usar task.ContinueWith , en lugar de la task.Result .

Ejemplo de locking previo:

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

Evite el punto muerto de esta manera:

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