understanding run net mvc example await async asp c# asp.net-mvc azure asynchronous async-await

run - understanding async await c#



ConfigureAwait(falso) en solicitudes de nivel superior (2)

Estoy tratando de averiguar si ConfigureAwait (falso) debería usarse en solicitudes de nivel superior. Leyendo esta publicación de una cierta autoridad del tema: http://blog.stephencleary.com/2012/07/dont-block-on-async-code.html

... recomienda algo como esto:

public async Task<JsonResult> MyControllerAction(...) { try { var report = await _adapter.GetReportAsync(); return Json(report, JsonRequestBehavior.AllowGet); } catch (Exception ex) { return Json("myerror", JsonRequestBehavior.AllowGet); // really slow without configure await } } public async Task<TodaysActivityRawSummary> GetReportAsync() { var data = await GetData().ConfigureAwait(false); return data }

... dice usar ConfigureAwait (falso) en cada espera excepto la llamada de nivel superior. Sin embargo, al hacer esto, mi excepción toma varios segundos para regresar a la persona que llama en lugar de usarla y hacer que vuelva enseguida.

¿Cuál es la mejor práctica para las acciones del controlador MVC que llaman a los métodos asíncronos? ¿Debería usar ConfigureAwait en el controlador mismo o solo en las llamadas de servicio que usan espera para solicitar datos, etc.? Si no lo uso en la llamada de nivel superior, esperar varios segundos para la excepción parece problemático. No necesito el HttpContext y he visto otras publicaciones que dicen que siempre usa ConfigureAwait (falso) si no necesita el contexto.

Actualización: me faltaba ConfigureAwait (falso) en algún lugar de mi cadena de llamadas que causaba que la excepción no se devolviera de inmediato. Sin embargo, la pregunta aún permanece publicada en cuanto a si ConfigureAwait (falso) debe o no usarse en el nivel superior.


Sin embargo, la pregunta aún permanece publicada en cuanto a si ConfigureAwait (falso) debe o no usarse en el nivel superior.

La regla de oro para ConfigureAwait(false) es usarla siempre que el resto de su método no necesite el contexto.

En ASP.NET, el "contexto" no está realmente bien definido en ninguna parte. Incluye cosas como HttpContext.Current , usuario principal y cultura del usuario.

Entonces, la pregunta realmente se reduce a: "¿ Controller.Json requiere el contexto ASP.NET?" Es ciertamente posible que a Json no le importe el contexto (ya que puede escribir la respuesta actual de sus propios miembros del controlador), pero OTOH sí "formatea", lo que puede requerir que se reanude la cultura del usuario.

No sé si Json requiere el contexto, pero no está documentado de una forma u otra, y en general asumo que cualquier llamada al código ASP.NET puede depender del contexto. Así que no usaría ConfigureAwait(false) en el nivel superior en mi código de controlador, solo para estar seguro.


¿Es un sitio web de alto tráfico? Una posible explicación podría ser que está experimentando la ThreadPool cuando no está utilizando ConfigureAwait(false) . Sin ConfigureAwait(false) , la continuación de await se pone en cola a través de AspNetSynchronizationContext.Post , cuya implementación se reduce a this :

Task newTask = _lastScheduledTask.ContinueWith(_ => SafeWrapCallback(action)); _lastScheduledTask = newTask; // the newly-created task is now the last one

Aquí, ContinueWith se usa sin TaskContinuationOptions.ExecuteSynchronously (especularía, para hacer continuidades realmente asíncronas y reducir la posibilidad de condiciones de baja acumulación). Por lo tanto, adquiere un hilo vacante de ThreadPool para ejecutar la continuación en. En teoría, podría pasar a ser el mismo hilo donde terminó la tarea antecedente para await , pero lo más probable es que sea un hilo diferente.

En este punto, si el grupo de subprocesos ASP.NET se está muriendo de hambre (o tiene que crecer para acomodar una nueva solicitud de subprocesos), es posible que experimente un retraso. Vale la pena mencionar que el grupo de subprocesos consta de dos subcolecciones: subprocesos de IOCP e hilos de trabajo (compruebe this y this para obtener más detalles). Es probable que sus operaciones GetReportAsync se completen en un subgrupo de subprocesos de IOCP, que no parece estar muriendo de hambre. OTOH, la continuación ContinueWith se ejecuta en un subconjunto de subprocesos de trabajo, que parece estar muriendo de hambre en su caso.

Esto no sucederá en caso de que ConfigureAwait(false) se use completamente. En ese caso, todas las continuaciones a la await se ejecutarán de forma síncrona en los mismos subprocesos que las tareas de antecedente correspondientes hayan finalizado, ya sea IOCP o subprocesos de trabajo.

Puede comparar el uso de subprocesos para ambos escenarios, con y sin ConfigureAwait(false) . Espero que este número sea mayor cuando ConfigureAwait(false) no se usa:

catch (Exception ex) { Log("Total number of threads in use={0}", Process.GetCurrentProcess().Threads.Count); return Json("myerror", JsonRequestBehavior.AllowGet); // really slow without configure await }

También puede intentar aumentar el tamaño del grupo de subprocesos de ASP.NET (para fines de diagnóstico, en lugar de una solución definitiva), para ver si el escenario descrito es realmente el caso aquí:

<configuration> <system.web> <applicationPool maxConcurrentRequestsPerCPU="6000" maxConcurrentThreadsPerCPU="0" requestQueueLimit="6000" /> </system.web> </configuration>

Actualizado para abordar los comentarios:

Me di cuenta de que me estaba perdiendo un ContinueAwait en algún lugar de mi cadena. Ahora funciona bien cuando se lanza una excepción incluso cuando el nivel superior no usa ConfigureAwait (falso).

Esto sugiere que su código o una biblioteca de terceros en uso podría estar utilizando construcciones de bloqueo ( Task.Result , Task.Wait , WaitHandle.WaitOne , quizás con alguna lógica de tiempo de espera adicional). ¿Has buscado esos? Pruebe la sugerencia Task.Run desde el final de esta actualización. Además, aún realizaría los diagnósticos de conteo de hilos para descartar la inanición / tartamudeo del hilo.

Entonces, ¿estás diciendo que si uso ContinueAwait incluso en el nivel superior, perderé todo el beneficio de la asincrónica?

No, no estoy diciendo eso. El objetivo de la async es evitar el bloqueo de hilos mientras se espera algo, y ese objetivo se logra independientemente del valor agregado de ContinueAwait(false) .

Lo que estoy diciendo es que no usar ConfigureAwait(false) podría introducir el cambio de contexto redundante (lo que generalmente significa cambio de subprocesos), que podría ser un problema en ASP.NET si el grupo de subprocesos está funcionando a su capacidad. Sin embargo, un conmutador de subproceso redundante es mejor que un subproceso bloqueado, en términos de escalabilidad del servidor.

Con toda justicia, el uso de ContinueAwait(false) también puede causar el cambio de contexto redundante, especialmente si se utiliza de manera incoherente en toda la cadena de llamadas.

Dicho esto, ContinueAwait(false) también se utiliza a menudo como un remedio contra los bloqueos provocados por el http://blog.stephencleary.com/2012/07/dont-block-on-async-code.html . Es por eso que sugerí más arriba que busque aquellos que bloquean la construcción en toda la base de códigos.

Sin embargo, la pregunta aún permanece publicada en cuanto a si ConfigureAwait (falso) debe o no usarse en el nivel superior.

Espero que Stephen Cleary pueda dar más detalles sobre esto, aquí están mis pensamientos.

Siempre hay un código de "nivel super-superior" que invoca su código de nivel superior. Por ejemplo, en el caso de una aplicación de interfaz de usuario, es el código de marco que invoca un controlador de eventos de async void . En el caso de ASP.NET, es BeginExecute del controlador asíncrono. Es responsabilidad de ese código super-nivel superior asegurarse de que, una vez que su tarea asincrónica haya finalizado, las continuaciones (si las hay) se ejecuten en el contexto de sincronización correcto. No es responsabilidad del código de su tarea. Por ejemplo, puede que no haya ninguna continuación en absoluto, como con un controlador de eventos async void disparar y olvidar; ¿Por qué te gustaría restaurar el contexto dentro de ese controlador?

Por lo tanto, dentro de los métodos de nivel superior, si no le importa el contexto para await continuas, utilice ConfigureAwait(false) tan pronto como sea posible.

Además, si está utilizando una biblioteca de terceros que se sabe que es independiente del contexto pero aún puede estar usando ConfigureAwait(false) incoherente, puede envolver la llamada con Task.Run o algo así como WithNoContext . Haría eso para sacar la cadena de las llamadas asincrónicas del contexto, de antemano:

var report = await Task.Run(() => _adapter.GetReportAsync()).ConfigureAwait(false); return Json(report, JsonRequestBehavior.AllowGet);

Esto introduciría un cambio de subproceso adicional, pero podría ahorrarte muchos más si ConfigureAwait(false) se utiliza de manera incoherente dentro de GetReportAsync o cualquiera de sus llamadas secundarias. También serviría como una solución alternativa para posibles bloqueos provocados por aquellos bloqueos constructivos dentro de la cadena de llamadas (si corresponde).

Sin embargo, tenga en cuenta que en ASP.NET HttpContext.Current no es la única propiedad estática que fluye con AspNetSynchronizationContext . Por ejemplo, también hay Thread.CurrentThread.CurrentCulture . Asegúrate de que realmente no te importa perder el contexto.

Actualizado para abordar el comentario:

Para los puntos de brownie, tal vez puedas explicar los efectos de ConfigureAwait (falso) ... ¿Qué contexto no se conserva ... ¿Es solo el HttpContext o las variables locales del objeto de la clase, etc.?

Todas las variables locales de un método async se conservan en await , así como la referencia implícita, por diseño. En realidad, se capturan en una estructura de máquina de estado asíncrono generada por el compilador, por lo que técnicamente no residen en la pila del hilo actual. En cierto modo, es similar a cómo un delegado de C # captura las variables locales. De hecho, una devolución de llamada de continuación en await es en sí misma un delegado pasado a ICriticalNotifyCompletion.UnsafeOnCompleted (implementado por el objeto que se está esperando; para Task , es TaskAwaiter ; con ConfigureAwait , está ConfiguredTaskAwaitable ).

OTOH, la mayor parte del estado global (variables estáticas / TLS, propiedades de clase estáticas) no fluye automáticamente a través de espera. Lo que fluye depende de un contexto de sincronización particular. En ausencia de uno (o cuando se usa ConfigureAwait(false) ), el único estado global conservado es el que ejecuta ExecutionContext . Stephen Toub de Microsoft tiene una gran publicación sobre eso: "ExecutionContext vs SynchronizationContext" . Menciona SecurityContext y Thread.CurrentPrincipal , que es crucial para la seguridad. Aparte de eso, no estoy al tanto de ninguna lista oficialmente documentada y completa de propiedades de estado global fluidas por ExecutionContext .

Puede echar un vistazo al origen de ExecutionContext.Capture para obtener más información acerca de qué es exactamente lo que fluye, pero no debe depender de esta implementación específica. En cambio, siempre puedes crear tu propia lógica de flujo de estado global, usando algo como AsyncLocal Stephen Cleary (o .NET 4.6 AsyncLocal<T> ).

O, para llevarlo al extremo, también podría abandonar ContinueAwait completo y crear un awaiter personalizado, por ejemplo, como ContinueOnScope . Eso permitiría tener un control preciso sobre qué hilo / contexto continuar y qué estado fluir.