¿Cómo usar API y patrones async / await que no sean seguros para subprocesos con ASP.NET Web API?

Esta pregunta ha sido activada por EF Data Context – Async / Await & Multithreading . He respondido eso, pero no he brindado ninguna solución definitiva.

El problema original es que hay muchas API .NET útiles (como DbContext Microsoft Entity Framework), que proporcionan métodos asíncronos diseñados para ser utilizados con la await , pero están documentados como no seguros para subprocesos . Eso los hace ideales para su uso en aplicaciones de escritorio UI, pero no para aplicaciones del lado del servidor. [EDITADO] Esto podría no aplicarse realmente a DbContext , aquí está la statement de Microsoft sobre seguridad de hilos EF6 , juzgue usted mismo. [/ EDITADO]

También hay algunos patrones de código establecidos que caen en la misma categoría, como llamar a un proxy de servicio WCF con OperationContextScope ( aquí y aquí ), por ejemplo:

 using (var docClient = CreateDocumentServiceClient()) using (new OperationContextScope(docClient.InnerChannel)) { return await docClient.GetDocumentAsync(docId); } 

Esto puede fallar porque OperationContextScope usa el almacenamiento local de subprocesos en su implementación.

El origen del problema es AspNetSynchronizationContext que se utiliza en las páginas ASP.NET asíncronas para satisfacer más solicitudes HTTP con menos subprocesos del grupo de subprocesos de ASP.NET . Con AspNetSynchronizationContext , una continuación en await se puede poner en cola en un hilo diferente del que inició la operación asincrónica, mientras que el hilo original se lanza al grupo y se puede usar para atender otra solicitud HTTP. Esto mejora sustancialmente la escalabilidad del código del lado del servidor. El mecanismo se describe en gran detalle en It’s All About SynchronizationContext , una lectura obligada. Por lo tanto, si bien no existe un acceso concurrente a la API , un posible cambio de hilo aún nos impide utilizar las API mencionadas anteriormente.

He estado pensando en cómo solucionar esto sin sacrificar la escalabilidad. Aparentemente, la única forma de recuperar esas API es mantener la afinidad del hilo por el scope de las llamadas asincrónicas potencialmente afectadas por un cambio de hilo.

Digamos que tenemos tal afinidad de hilos. La mayoría de esas llamadas están vinculadas a IO de todos modos ( No hay subproceso ). Mientras una tarea asíncrona está pendiente, el hilo en el que se originó se puede usar para servir una continuación de otra tarea similar, cuyo resultado ya está disponible. Por lo tanto, no debería perjudicar demasiado la escalabilidad. Este enfoque no es nada nuevo, de hecho, un modelo de subproceso único similar es utilizado con éxito por Node.js. IMO, esta es una de esas cosas que hacen que Node.js sea tan popular.

No veo por qué este enfoque no pudo usarse en el contexto de ASP.NET. Un progtwigdor de tareas personalizado (llamémoslo ThreadAffinityTaskScheduler ) podría mantener un grupo separado de subprocesos de “apartamento de afinidad” para mejorar la escalabilidad aún más. Una vez que la tarea ha sido puesta en cola a uno de esos hilos de “apartamento”, todos await continúen dentro de la tarea que tendrá lugar en el mismo hilo.

Así es como se puede usar una API que no sea segura para subprocesos a partir de la pregunta vinculada con dicho ThreadAffinityTaskScheduler :

 // create a global instance of ThreadAffinityTaskScheduler - per web app public static class GlobalState { public static ThreadAffinityTaskScheduler TaScheduler { get; private set; } public static GlobalState { GlobalState.TaScheduler = new ThreadAffinityTaskScheduler( numberOfThreads: 10); } } // ... // run a task which uses non-thread-safe APIs var result = await GlobalState.TaScheduler.Run(() => { using (var dataContext = new DataContext()) { var something = await dataContext.someEntities.FirstOrDefaultAsync(e => e.Id == 1); var morething = await dataContext.someEntities.FirstOrDefaultAsync(e => e.Id == 2); // ... // transform "something" and "morething" into thread-safe objects and return the result return data; } }, CancellationToken.None); 

Seguí adelante e implementé ThreadAffinityTaskScheduler como una prueba de concepto , basada en el excelente StaTaskScheduler Stephen Toub. Los subprocesos de agrupación mantenidos por ThreadAffinityTaskScheduler no son subprocesos STA en el sentido clásico de COM, pero sí implementan afinidad de subprocesos para las continuaciones en await ( SingleThreadSynchronizationContext es responsable de ello).

Hasta ahora, he probado este código como una aplicación de consola y parece funcionar como está diseñado. Todavía no lo he probado en una página ASP.NET. No tengo mucha experiencia en desarrollo de ASP.NET de producción, así que mis preguntas son:

  1. ¿Tiene sentido utilizar este enfoque sobre la invocación síncrona simple de API no seguras para subprocesos en ASP.NET (el objective principal es evitar sacrificar la escalabilidad)?

  2. ¿Hay enfoques alternativos, además de utilizar invocaciones de API síncronas o evitar esos APis en absoluto?

  3. ¿Alguien ha usado algo similar en los proyectos ASP.NET MVC o Web API y está listo para compartir su experiencia?

  4. Se agradecerá cualquier consejo sobre cómo probar la tensión y perfilar este enfoque con ASP.NET.

Entity Framework (debería) manejar los saltos de subprocesos en los puntos de await muy bien; si no lo hace, entonces eso es un error en EF. OTOH, OperationContextScope se basa en TLS y no está a la await -seguro.

1. Las API síncronas mantienen su contexto ASP.NET; esto incluye cosas como la identidad del usuario y la cultura que a menudo son importantes durante el procesamiento. Además, varias API ASP.NET suponen que se están ejecutando en un contexto ASP.NET real (no me refiero solo a usar HttpContext.Current ; me refiero, en realidad, suponiendo que SynchronizationContext.Current es una instancia de AspNetSynchronizationContext ).

2-3. He utilizado mi propio contexto de subproceso único nested directamente en el contexto de ASP.NET, en un bash de que las acciones secundarias de MVC async funcionen sin tener que duplicar el código. Sin embargo, no solo pierde los beneficios de escalabilidad (al menos para esa parte de la solicitud), también se encuentra con las API ASP.NET suponiendo que se ejecutan en un contexto ASP.NET.

Entonces, nunca he usado este enfoque en producción. Acabo de terminar usando las API síncronas cuando es necesario.

No debe entrelazar subprocesos múltiples con asincronía. El problema con un objeto que no es seguro para subprocesos es cuando se accede a una sola instancia (o estática) por varios subprocesos al mismo tiempo . Con las llamadas asincrónicas, posiblemente se acceda al contexto desde un subproceso diferente en la continuación, pero nunca al mismo tiempo (cuando no se comparte entre varias solicitudes, pero eso no es bueno en primer lugar).