Se recomienda llamar a ConfigureAwait para todo el código del lado del servidor

561

Cuando tiene un código del lado del servidor (es decir, algunos ApiController) y sus funciones son asíncronas, por lo que regresan Task<SomeObject>, ¿se considera la mejor práctica que siempre que espere funciones que llame ConfigureAwait(false)?

Había leído que es más eficiente ya que no tiene que cambiar los contextos de hilo al contexto de hilo original. Sin embargo, con ASP.NET Web Api, si su solicitud llega en un hilo, y espera alguna función y llamada ConfigureAwait(false)que podría ponerlo en un hilo diferente cuando devuelva el resultado final de su ApiControllerfunción.

He escrito un ejemplo de lo que estoy hablando a continuación:

public class CustomerController : ApiController
{
    public async Task<Customer> Get(int id)
    {
        // you are on a particular thread here
        var customer = await SomeAsyncFunctionThatGetsCustomer(id).ConfigureAwait(false);

        // now you are on a different thread!  will that cause problems?
        return customer;
    }
}
Arash Emami
fuente

Respuestas:

628

Actualización: ASP.NET Core no tiene unSynchronizationContext . Si está en ASP.NET Core, no importa si lo usa ConfigureAwait(false)o no.

Para ASP.NET "Completo" o "Clásico" o lo que sea, el resto de esta respuesta aún se aplica.

Publicación original (para ASP.NET no Core):

Este video del equipo de ASP.NET tiene la mejor información sobre el uso asyncen ASP.NET.

Había leído que es más eficiente ya que no tiene que cambiar los contextos de hilo al contexto de hilo original.

Esto es cierto con las aplicaciones de interfaz de usuario, donde solo hay un hilo de interfaz de usuario al que debe "sincronizar" nuevamente.

En ASP.NET, la situación es un poco más compleja. Cuando un asyncmétodo reanuda la ejecución, toma un hilo del grupo de hilos ASP.NET. Si deshabilita el uso de la captura de contexto ConfigureAwait(false), entonces el hilo simplemente continúa ejecutando el método directamente. Si no deshabilita la captura de contexto, el hilo volverá a ingresar el contexto de solicitud y luego continuará ejecutando el método.

Por ConfigureAwait(false)lo tanto, no le ahorra un salto de hilo en ASP.NET; le ahorra el reingreso del contexto de solicitud, pero esto normalmente es muy rápido. ConfigureAwait(false) podría ser útil si está intentando hacer una pequeña cantidad de procesamiento paralelo de una solicitud, pero realmente TPL es una mejor opción para la mayoría de esos escenarios.

Sin embargo, con ASP.NET Web Api, si su solicitud llega en un hilo, y espera alguna función y llama a ConfigureAwait (falso) que podría ponerlo en un hilo diferente cuando devuelva el resultado final de su función ApiController .

En realidad, solo hacer un awaitpuede hacer eso. Una vez que su asyncmétodo llega a un await, el método se bloquea pero el subproceso vuelve al grupo de subprocesos. Cuando el método está listo para continuar, cualquier subproceso se extrae del grupo de subprocesos y se utiliza para reanudar el método.

La única diferencia que ConfigureAwaithace en ASP.NET es si ese hilo ingresa al contexto de solicitud al reanudar el método.

Tengo más información de fondo en mi artículo de MSDNSynchronizationContext y en mi asyncentrada de blog de introducción .

Stephen Cleary
fuente
23
El almacenamiento local de subprocesos no fluye por ningún contexto. HttpContext.Currentes operado por ASP.NET SynchronizationContext, que fluye de manera predeterminada cuando usted await, pero no lo hace ContinueWith. OTOH, el contexto de ejecución (incluidas las restricciones de seguridad) es el contexto mencionado en CLR a través de C #, y es fluido por ambos ContinueWithy await(incluso si lo usa ConfigureAwait(false)).
Stephen Cleary
65
¿No sería genial si C # tuviera soporte de idioma nativo para ConfigureAwait (falso)? Algo como 'awaitnc' (no espere contexto). Escribir una llamada a un método separado en todas partes es bastante molesto. :)
NathanAldenSr
19
@NathanAldenSr: Se discutió bastante. El problema con una nueva palabra clave es que en ConfigureAwaitrealidad solo tiene sentido cuando espera tareas , mientras que awaitactúa en cualquier "espera". Otras opciones consideradas fueron: ¿Debería el comportamiento predeterminado descartar el contexto si se encuentra en una biblioteca? ¿O tiene una configuración de compilador para el comportamiento de contexto predeterminado? Ambos fueron rechazados porque es más difícil simplemente leer el código y decir lo que hace.
Stephen Cleary
10
@AnshulNigam: por eso las acciones del controlador necesitan su contexto. Pero la mayoría de los métodos que las acciones llaman no lo hacen.
Stephen Cleary
14
@JonathanRoeder: en términos generales, no debería necesitar ConfigureAwait(false)evitar un punto muerto basado en Result/ Waitporque en ASP.NET no debería usar Result/ Waiten primer lugar.
Stephen Cleary
131

Breve respuesta a su pregunta: No. No debe llamar ConfigureAwait(false)al nivel de aplicación de esa manera.

TL; Versión DR de la respuesta larga: si está escribiendo una biblioteca donde no conoce a su consumidor y no necesita un contexto de sincronización (que no debería en una biblioteca, creo), siempre debe usar ConfigureAwait(false). De lo contrario, los consumidores de su biblioteca pueden enfrentar puntos muertos al consumir sus métodos asincrónicos de manera bloqueante. Esto depende de la situación.

Aquí hay una explicación un poco más detallada sobre la importancia del ConfigureAwaitmétodo (una cita de mi publicación de blog):

Cuando está esperando un método con la palabra clave await, el compilador genera un montón de código en su nombre. Uno de los propósitos de esta acción es manejar la sincronización con el hilo UI (o principal). El componente clave de esta característica es el SynchronizationContext.Currentque obtiene el contexto de sincronización para el hilo actual. SynchronizationContext.Currentse rellena según el entorno en el que se encuentre. El GetAwaitermétodo de Tarea busca SynchronizationContext.Current. Si el contexto de sincronización actual no es nulo, la continuación que se pasa a ese camarero se volverá a publicar en ese contexto de sincronización.

Al consumir un método, que utiliza las nuevas funciones de lenguaje asincrónico, de forma bloqueante, terminará en un punto muerto si tiene un SynchronizationContext disponible. Cuando está consumiendo dichos métodos de manera bloqueante (esperando en el método Tarea con Espera o tomando el resultado directamente de la propiedad Resultado de la Tarea), bloqueará el hilo principal al mismo tiempo. Cuando finalmente la Tarea se complete dentro de ese método en el conjunto de subprocesos, va a invocar la continuación para publicar nuevamente en el subproceso principal porque SynchronizationContext.Currentestá disponible y capturado. Pero hay un problema aquí: ¡el hilo de la interfaz de usuario está bloqueado y tienes un punto muerto!

Además, aquí hay dos excelentes artículos para usted que son exactamente para su pregunta:

Finalmente, hay un gran video corto de Lucian Wischik exactamente sobre este tema: los métodos de la biblioteca asíncrona deberían considerar el uso de Task.ConfigureAwait (falso) .

Espero que esto ayude.

tugberk
fuente
2
"El método de tarea GetAwaiter busca SynchronizationContext.Current. Si el contexto de sincronización actual no es nulo, la continuación que se pasa a ese camarero se volverá a publicar en ese contexto de sincronización". - Tengo la impresión de que estás tratando de decir que Taskcamina por la pila para obtener el SynchronizationContext, que está mal. Se SynchronizationContexttoma antes de la llamada al Tasky luego se continúa con el resto del código SynchronizationContextsi SynchronizationContext.Currentno es nulo.
casperOne
1
@casperOne Tengo la intención de decir lo mismo.
tugberk
8
¿No debería ser responsabilidad de la persona que llama asegurarse de que SynchronizationContext.Currentesté claro / o que la biblioteca se llame dentro de un en Task.Run()lugar de tener que escribir en .ConfigureAwait(false)toda la biblioteca de la clase?
binki
1
@binki - por otro lado: (1) presumiblemente se usa una biblioteca en muchas aplicaciones, por lo que hacer un esfuerzo una vez en la biblioteca para facilitar las aplicaciones es rentable; (2) presumiblemente, el autor de la biblioteca sabe que ha escrito un código que no tiene ninguna razón para exigir que continúe en el contexto original, que expresa por esos .ConfigureAwait(false)s. Quizás sería más fácil para los autores de bibliotecas si ese fuera el comportamiento predeterminado, pero supongo que hacer un poco más difícil escribir una biblioteca correctamente es mejor que hacer un poco más difícil escribir una aplicación correctamente.
ToolmakerSteve
44
¿Por qué el autor de una biblioteca molesta al consumidor? Si el consumidor quiere un punto muerto, ¿por qué debería evitarlo?
Quarkly
25

El mayor inconveniente que he encontrado al usar ConfigureAwait (falso) es que la cultura del hilo se revierte a los valores predeterminados del sistema. Si ha configurado una cultura, por ejemplo ...

<system.web>
    <globalization culture="en-AU" uiCulture="en-AU" />    
    ...

y está alojando en un servidor cuya cultura está configurada en en-EE. UU., Entonces encontrará antes de que ConfigureAwait (falso) se llame CultureInfo. es decir

// CultureInfo.CurrentCulture ~ {en-AU}
await xxxx.ConfigureAwait(false);
// CultureInfo.CurrentCulture ~ {en-US}

Si su aplicación está haciendo algo que requiere un formato de datos específico de la cultura, deberá tener esto en cuenta al usar ConfigureAwait (falso).

Mick
fuente
27
Las versiones modernas de .NET (creo que desde 4.6?) Propagarán la cultura a través de hilos, incluso si ConfigureAwait(false)se usa.
Stephen Cleary
1
Gracias por la info. De hecho, estamos utilizando .net 4.5.2
Mick
11

Tengo algunas ideas generales sobre la implementación de Task:

  1. La tarea es desechable pero no se supone que la usemos using.
  2. ConfigureAwaitse introdujo en 4.5. Taskfue introducido en 4.0.
  3. Los subprocesos .NET siempre se utilizan para hacer fluir el contexto (consulte C # a través del libro CLR), pero en la implementación predeterminada de Task.ContinueWithb / c no se dio cuenta de que el cambio de contexto es costoso y está desactivado de forma predeterminada.
  4. El problema es que un desarrollador de bibliotecas no debería preocuparse si sus clientes necesitan flujo de contexto o no, por lo tanto, no debería decidir si fluye el contexto o no.
  5. [Agregado más tarde] El hecho de que no haya una respuesta autorizada y una referencia adecuada y sigamos luchando sobre esto significa que alguien no ha hecho bien su trabajo.

Tengo algunas publicaciones sobre el tema, pero mi opinión, además de la buena respuesta de Tugberk, es que debes convertir todas las API en asíncronas e idealmente fluir el contexto. Como está haciendo asíncrono, simplemente puede usar continuaciones en lugar de esperar para que no se produzca un punto muerto, ya que no se realiza ninguna espera en la biblioteca y se mantiene el flujo para que se conserve el contexto (como HttpContext).

El problema es cuando una biblioteca expone una API síncrona pero usa otra API asíncrona; por lo tanto, debe usar Wait()/ Resulten su código.

Aliostad
fuente
66
1) Puedes llamar Task.Disposesi quieres; simplemente no necesitas la gran mayoría de las veces. 2) Taskse introdujo en .NET 4.0 como parte de la TPL, que no necesitaba ConfigureAwait; cuando asyncse agregó, reutilizaron el Tasktipo existente en lugar de inventar uno nuevo Future.
Stephen Cleary
66
3) Estás confundiendo dos tipos diferentes de "contexto". El "contexto" mencionado en C # a través de CLR siempre fluye, incluso en Tasks; el "contexto" controlado por ContinueWithes un SynchronizationContexto TaskScheduler. Estos diferentes contextos se explican en detalle en el blog de Stephen Toub .
Stephen Cleary
21
4) El autor de la biblioteca no necesita preocuparse si sus llamantes necesitan el flujo de contexto, porque cada método asincrónico se reanuda de forma independiente. Entonces, si las personas que llaman necesitan el flujo de contexto, pueden hacerlo, independientemente de si el autor de la biblioteca lo hizo o no.
Stephen Cleary
1
Al principio, parece que te estás quejando en lugar de responder la pregunta. Y luego estás hablando de "el contexto", excepto que hay varios tipos de contexto en .Net y realmente no está claro de cuál (¿o cuáles?) Estás hablando. E incluso si usted no está confundido (pero creo que sí, creo que no hay contexto que solía fluir con Threads, pero ya no lo hace con ContinueWith()), esto hace que su respuesta sea confusa de leer.
svick
1
@StephenCleary sí, lib dev no debería necesitar saberlo, depende del cliente. Pensé que lo había dejado claro, pero mi fraseo no estaba claro.
Aliostad