Contexto de datos EF – Async / Await y Multithreading

Frecuentemente utilizo async / await para asegurarme de que los hilos de la API web ASP.NET MVC no estén bloqueados por las operaciones de E / S y de red de más larga duración, específicamente las llamadas a la base de datos.

El espacio de nombres System.Data.Entity proporciona una variedad de extensiones de ayuda aquí, como FirstOrDefaultAsync , ContainsAsync , CountAsync , etc.

Sin embargo, dado que los contextos de datos no son seguros para subprocesos, esto significa que el siguiente código es problemático:

var dbContext = new DbContext(); var something = await dbContext.someEntities.FirstOrDefaultAsync(e => e.Id == 1); var morething = await dbContext.someEntities.FirstOrDefaultAsync(e => e.Id == 2); 

De hecho, a veces veo excepciones tales como:

System.InvalidOperationException: la conexión no se cerró. El estado actual de la conexión está abierto.

¿Es el patrón correcto para usar un bloque de uso separado using(new DbContext...) para cada llamada asincrónica a la base de datos? ¿Es potencialmente más beneficioso simplemente ejecutar síncrono entonces?

Aquí tenemos una situación estancada. AspNetSynchronizationContext , que es responsable del modelo de subprocesamiento de un entorno de ejecución de API Web ASP.NET, no garantiza que la continuación asincrónica después de la await se realice en el mismo subproceso. La idea general de esto es hacer que las aplicaciones ASP.NET sean más escalables, por ThreadPool se bloquean menos hilos de ThreadPool con operaciones sincrónicas pendientes.

Sin embargo, DataContext no es seguro para subprocesos, por lo que no se debe usar donde potencialmente se pueda producir un cambio de subprocesos a través de las llamadas a la API de DataContext . Una construcción de using separada por llamada asincrónica no ayudará, tampoco:

 var something; using (var dataContext = new DataContext()) { something = await dataContext.someEntities.FirstOrDefaultAsync(e => e.Id == 1); } 

Esto se debe a que DataContext.Dispose podría ejecutarse en un hilo diferente del que el objeto fue originalmente creado, y esto no es algo que DataContext esperaría.

Si te gusta seguir con la API de DataContext , llamarla de forma síncrona parece ser la única opción viable. No estoy seguro de si esa statement debería extenderse a toda la API de EF, pero supongo que cualquier objeto hijo creado con DataContext API probablemente tampoco sea seguro para subprocesos. Por lo tanto, en ASP.NET, su ámbito de aplicación debería limitarse a la de entre dos llamadas en await adyacentes.

Podría ser tentador descargar un montón de llamadas DataContext sincrónicas a un hilo separado con await Task.Run(() => { /* do DataContext stuff here */ }) . Sin embargo, sería un antipatrón , especialmente en el contexto de ASP.NET, donde podría perjudicar el rendimiento y la escalabilidad, ya que no reduciría el número de subprocesos necesarios para cumplir con la solicitud.

Desafortunadamente, si bien la architecture asincrónica de ASP.NET es excelente, sigue siendo incompatible con algunas API y patrones establecidos (por ejemplo, aquí hay un caso similar ). Eso es especialmente triste, porque aquí no nos ocupamos del acceso concurrente a la API, es decir, no más de un hilo intenta acceder a un objeto DataContext al mismo tiempo.

Con suerte, Microsoft abordará eso en las futuras versiones del Framework.

[ACTUALIZACIÓN] A gran escala, sin embargo, podría ser posible descargar la lógica EF a un proceso separado (ejecutado como un servicio WCF) que proporcionaría una API asíncrona segura para subprocesos a la lógica del cliente ASP.NET. Tal proceso puede ser orquestado con un contexto de sincronización personalizado como una máquina de eventos, similar a Node.js. Incluso puede ejecutar un conjunto de apartamentos tipo Node.js, cada apartamento mantiene la afinidad de los objetos EF. Eso permitiría aún beneficiarse de la async EF API.

[ACTUALIZAR] Aquí hay algún bash de encontrar una solución a este problema.

La clase DataContext es parte de LINQ to SQL. No comprende async / await AFAIK, y no debe utilizarse con los métodos de extensión async Entity Framework.

La clase DbContext funcionará bien con async siempre que use EF6 o superior; sin embargo, solo puede tener una operación (sincronización o DbContext ) por instancia de DbContext ejecute a la vez. Si su código está realmente usando DbContext , entonces examine la stack de llamadas de su excepción y verifique si hay algún uso simultáneo (por ejemplo, Task.WhenAll ).

Si está seguro de que todo el acceso es secuencial, publique una reproducción mínima o infórmelo como un error en Microsoft Connect.