Hola Habr! Le presento la traducción del artículo de
Preguntas frecuentes sobre ConfigureAwait de Stephen Taub.

Async
/
await
agregó a .NET hace más de siete años. Esta decisión ha tenido un impacto significativo no solo en el ecosistema .NET, sino que también se refleja en muchos otros lenguajes y marcos. Actualmente, se han implementado muchas mejoras en .NET en términos de construcciones de lenguaje adicionales usando asincronía, se han implementado API con soporte de asincronía, se han realizado mejoras fundamentales en la infraestructura debido a que
async
/
await
funciona como un reloj (en particular, se han mejorado las capacidades de rendimiento y diagnóstico en .NET Core).
ConfigureAwait
es un aspecto de
async
/
await
que continúa generando preguntas. Espero poder responder a muchas de ellas. Intentaré que este artículo sea legible de principio a fin y, al mismo tiempo, ejecutarlo al estilo de las respuestas a las preguntas frecuentes (FAQ) para que pueda ser referenciado en el futuro.
Para tratar realmente con
ConfigureAwait
, volveremos un poco.
¿Qué es un SynchronizationContext?
De acuerdo con la documentación de
System.Threading.SynchronizationContext "Proporciona funcionalidad básica para distribuir el contexto de sincronización en varios modelos de sincronización". Esta definición no es del todo obvia.
En el 99.9% de los casos, el
SynchronizationContext
usa simplemente como un tipo con un método
Post
virtual que acepta un delegado para la ejecución asincrónica (hay otros miembros virtuales en el
SynchronizationContext
, pero son menos comunes y no se discutirán en este artículo). El método
Post
del tipo base literalmente
simplemente llama a ThreadPool.QueueUserWorkItem
para ejecutar asincrónicamente el delegado proporcionado. Los tipos derivados anulan
Post
para que el delegado pueda ejecutar en el lugar correcto en el momento correcto.
Por ejemplo, Windows Forms tiene un
tipo derivado de SynchronizationContext que redefine
Post
para hacer el equivalente de
Control.BeginInvoke
. Esto significa que cualquier llamada a este método
Post
dará como resultado una llamada al delegado en una etapa posterior en el hilo asociado con el Control correspondiente, el llamado hilo de UI. En el corazón de Windows Forms está el procesamiento de mensajes Win32. El bucle de mensajes se ejecuta en un subproceso de interfaz de usuario que solo espera a que se procesen nuevos mensajes. Estos mensajes se activan por el movimiento del mouse, los clics, la entrada del teclado, los eventos del sistema que están disponibles para la ejecución de los delegados, etc. Por lo tanto, si tiene una instancia de
SynchronizationContext
para un subproceso de interfaz de usuario en una aplicación de formularios Windows Forms, debe pasar el delegado al método
Post
para realizar una operación en él.
Windows Presentation Foundation (WPF) también tiene un
tipo derivado de
SynchronizationContext
con un método de
Post
anulado que de manera similar "dirige" al delegado a la secuencia de la interfaz de usuario (usando
Dispatcher.BeginInvoke
), con el control WPF Dispatcher, no Windows Forms Control.
Y Windows RunTime (WinRT) tiene su propio
tipo derivado de
SynchronizationContext
, que también coloca al delegado en la
CoreDispatcher
subprocesos de la interfaz de usuario utilizando
CoreDispatcher
.
Esto es lo que hay detrás de la frase "ejecutar delegado en el hilo de la interfaz de usuario". También puede implementar su
SynchronizationContext
con el método
Post
y alguna implementación. Por ejemplo, no tengo que preocuparme por el hilo en el que se está ejecutando el delegado, pero quiero asegurarme de que cualquier método
Post
delegado en mi
SynchronizationContext
ejecute con cierto grado de paralelismo limitado. Puede implementar un
SynchronizationContext
personalizado de esta manera:
internal sealed class MaxConcurrencySynchronizationContext : SynchronizationContext { private readonly SemaphoreSlim _semaphore; public MaxConcurrencySynchronizationContext(int maxConcurrencyLevel) => _semaphore = new SemaphoreSlim(maxConcurrencyLevel); public override void Post(SendOrPostCallback d, object state) => _semaphore.WaitAsync().ContinueWith(delegate { try { d(state); } finally { _semaphore.Release(); } }, default, TaskContinuationOptions.None, TaskScheduler.Default); public override void Send(SendOrPostCallback d, object state) { _semaphore.Wait(); try { d(state); } finally { _semaphore.Release(); } } }
El marco xUnit tiene una
implementación similar
de SynchronizationContext. Aquí se usa para reducir la cantidad de código asociado con las pruebas paralelas.
Las ventajas aquí son las mismas que con cualquier abstracción: se proporciona una única API que puede usarse para poner en cola al delegado para su ejecución de la manera que el programador lo desee, sin la necesidad de conocer los detalles de implementación. Supongamos que escribo una biblioteca donde necesito hacer algo de trabajo y luego vuelvo a poner un delegado en el contexto original. Para hacer esto, necesito capturar su
SynchronizationContext
, y cuando complete lo que se necesita, solo tendré que llamar al método
Post
de este contexto y pasarle un delegado para su ejecución. No necesito saber que para Windows Forms necesitas tomar
Control
y usar su
BeginInvoke
, para WPF usar
BeginInvoke
de
Dispatcher
, o de alguna manera obtener el contexto y su cola para xUnit. Todo lo que necesito hacer es tomar el
SynchronizationContext
actual y usarlo más tarde. Para hacer esto, el
SynchronizationContext
tiene una propiedad
Current
. Esto se puede implementar de la siguiente manera:
public void DoWork(Action worker, Action completion) { SynchronizationContext sc = SynchronizationContext.Current; ThreadPool.QueueUserWorkItem(_ => { try { worker(); } finally { sc.Post(_ => completion(), null); } }); }
Puede establecer un contexto especial desde la propiedad
Current
utilizando el método
SynchronizationContext.SetSynchronizationContext
.
¿Qué es un programador de tareas?
SynchronizationContext
es una abstracción común para el "planificador". Algunos marcos usan sus propias abstracciones para ello, y
System.Threading.Tasks
no
System.Threading.Tasks
una excepción. Cuando hay delegados en la
Task
que se pueden poner en cola y ejecutar, están asociados con
System.Threading.Tasks.TaskScheduler
. También hay un método
Post
virtual para poner en cola a un delegado (una llamada de delegado se implementa utilizando mecanismos estándar),
TaskScheduler
proporciona un método abstracto
QueueTask
(una llamada de tarea se implementa usando el método
ExecuteTask
).
El planificador predeterminado que devuelve
TaskScheduler.Default
es un grupo de subprocesos. Desde
TaskScheduler
también es posible obtener y anular métodos para configurar la hora y el lugar de la llamada de la
Task
. Por ejemplo, las bibliotecas principales incluyen el tipo
System.Threading.Tasks.ConcurrentExclusiveSchedulerPair
. Una instancia de esta clase proporciona dos propiedades de
TaskScheduler
:
ExclusiveScheduler
y
ConcurrentScheduler
. Las tareas programadas en el
ConcurrentScheduler
se pueden realizar en paralelo, pero teniendo en cuenta la restricción establecida por el
ConcurrentExclusiveSchedulerPair
cuando se crea (similar a
MaxConcurrencySynchronizationContext
). No se ejecutará ninguna tarea
ConcurrentScheduler
si la tarea se ejecuta en
ExclusiveScheduler
y solo se permite ejecutar una tarea exclusiva a la vez. Este comportamiento es muy similar a un bloqueo de lectura / escritura.
Al igual que
SynchronizationContext
,
TaskScheduler
tiene una propiedad
Current
que devuelve el
TaskScheduler
actual. Sin embargo, a diferencia de
SynchronizationContext
, carece de un método para configurar el planificador actual. En cambio, el planificador está asociado con la tarea actual. Entonces, por ejemplo, este programa mostrará
True
, ya que la lambda utilizada en
StartNew
se ejecuta en la instancia
ExclusiveScheduler
de
ConcurrentExclusiveSchedulerPair
, y
TaskScheduler.Current
instalado en este programador:
using System; using System.Threading.Tasks; class Program { static void Main() { var cesp = new ConcurrentExclusiveSchedulerPair(); Task.Factory.StartNew(() => { Console.WriteLine(TaskScheduler.Current == cesp.ExclusiveScheduler); }, default, TaskCreationOptions.None, cesp.ExclusiveScheduler).Wait(); } }
Curiosamente,
TaskScheduler
proporciona un método estático
FromCurrentSynchronizationContext
. El método crea un nuevo
TaskScheduler
y
TaskScheduler
en
TaskScheduler
las tareas para su ejecución en el contexto
SynchronizationContext.Current
devuelto utilizando el método
Post
.
¿Cómo se relacionan el SynchronizationContext y TaskScheduler con wait?
Digamos que necesita escribir una aplicación de IU con un botón. Al presionar el botón, se inicia la descarga de texto del sitio web y se establece en el botón
Content
. Solo se debe poder acceder al botón desde la interfaz de usuario de la transmisión en la que se encuentra, por lo tanto, cuando cargamos con éxito la fecha y la hora y queremos colocarlos en el
Content
del botón, debemos hacerlo desde la transmisión que tiene control sobre él. Si no se cumple esta condición, obtendremos una excepción:
System.InvalidOperationException: ' , .'
Podemos usar manualmente
SynchronizationContext
para establecer el
Content
en el contexto de origen, por ejemplo a través de
TaskScheduler
:
private static readonly HttpClient s_httpClient = new HttpClient(); private void downloadBtn_Click(object sender, RoutedEventArgs e) { s_httpClient.GetStringAsync("http://example.com/currenttime").ContinueWith(downloadTask => { downloadBtn.Content = downloadTask.Result; }, TaskScheduler.FromCurrentSynchronizationContext()); }
Y podemos usar el
SynchronizationContext
directamente:
private static readonly HttpClient s_httpClient = new HttpClient(); private void downloadBtn_Click(object sender, RoutedEventArgs e) { SynchronizationContext sc = SynchronizationContext.Current; s_httpClient.GetStringAsync("http://example.com/currenttime").ContinueWith(downloadTask => { sc.Post(delegate { downloadBtn.Content = downloadTask.Result; }, null); }); }
Sin embargo, ambas opciones usan explícitamente una devolución de llamada. En cambio, podemos usar
async
/
await
:
private static readonly HttpClient s_httpClient = new HttpClient(); private async void downloadBtn_Click(object sender, RoutedEventArgs e) { string text = await s_httpClient.GetStringAsync("http://example.com/currenttime"); downloadBtn.Content = text; }
Todo esto "simplemente funciona" y configura con éxito el
Content
en el hilo de la interfaz de usuario, ya que en el caso de la versión implementada manualmente anterior, por defecto, la espera de una tarea se refiere a
SynchronizationContext.Current
y
TaskScheduler.Current
. Cuando "espera" algo en C #, el compilador convierte el código para sondeo (llamando a
GetAwaiter
) "esperado" (en este caso, Tarea) a "esperando" (
TaskAwaiter
). La "espera" es responsable de adjuntar una devolución de llamada (a menudo llamada "continuación") que vuelve a llamar a la máquina de estado cuando se completa la espera. Implementa esto usando el contexto / planificador que capturó durante el registro de devolución de llamada. Optimizaremos y configuraremos un poco, es algo como esto:
object scheduler = SynchronizationContext.Current; if (scheduler is null && TaskScheduler.Current != TaskScheduler.Default) { scheduler = TaskScheduler.Current; }
Aquí, primero se verifica si
SynchronizationContext
y, si no, si
TaskScheduler
no estándar. Si hay uno, entonces cuando la devolución de llamada esté lista para la llamada, se utilizará el planificador capturado; si no, la devolución de llamada se ejecutará como parte de la operación que completa la tarea esperada.
¿Qué hace ConfigureAwait (falso)?
El método
ConfigureAwait
no es especial: el compilador o el tiempo de ejecución no lo reconocen de ninguna manera en particular. Este es un método normal que devuelve una estructura (
ConfiguredTaskAwaitable
- ajusta la tarea original) y toma un valor booleano. Recuerde que
await
puede usarse con cualquier tipo que implemente el patrón correcto. Si se devuelve otro tipo, significa que cuando el compilador obtiene acceso al método
GetAwaiter
(parte del patrón) de las instancias, pero lo hace desde el tipo devuelto por
ConfigureAwait
y no desde la tarea directamente. Esto le permite cambiar el comportamiento de
await
para este camarero especial.
Esperar el tipo devuelto por
ConfigureAwait(continueOnCapturedContext: false)
lugar de esperar
Task
afecta directamente la implementación de captura de contexto / planificador discutida anteriormente. La lógica se convierte en algo como esto:
object scheduler = null; if (continueOnCapturedContext) { scheduler = SynchronizationContext.Current; if (scheduler is null && TaskScheduler.Current != TaskScheduler.Default) { scheduler = TaskScheduler.Current; } }
En otras palabras, especificar
false
, incluso si hay un contexto actual o un planificador para la devolución de llamada, implica que está ausente.
¿Por qué necesito usar ConfigureAwait (falso)?
ConfigureAwait(continueOnCapturedContext: false)
usa para evitar que la devolución de llamada se vea obligada a llamar en el contexto de origen o el planificador. Esto nos da varias ventajas:
Mejora de rendimiento. Hay una sobrecarga de poner en cola una devolución de llamada, a diferencia de solo llamar, ya que esto requiere un trabajo adicional (y generalmente una asignación adicional). Además, no podemos usar la optimización en tiempo de ejecución (podemos optimizar más cuando sabemos exactamente cómo se llamará la devolución de llamada, pero si se pasa a una implementación arbitraria de abstracción, a veces esto impone restricciones). Para secciones muy cargadas, incluso los costos adicionales de verificar el
SynchronizationContext
actual y el
TaskScheduler
actual (que implican acceso a las estadísticas de flujo) pueden aumentar significativamente la sobrecarga. Si el código después de
await
no requiere ejecución en el contexto original, usando
ConfigureAwait(false)
, se pueden evitar todos estos gastos, ya que no es necesario ponerlo en cola innecesariamente, puede usar todas las optimizaciones disponibles y también puede evitar el acceso innecesario a las estadísticas de flujo.
Prevención de punto muerto. Considere el método de biblioteca que
await
usar para descargar algo de la red. Llama a este método y bloquea sincrónicamente, esperando que la tarea se complete, por ejemplo, usando
.Wait()
o
.Result
o
.GetAwaiter()
.GetResult()
. Ahora considere lo que sucede si la llamada ocurre cuando el
SynchronizationContext
actual limita el número de operaciones a 1 explícitamente usando
MaxConcurrencySynchronizationContext
, o implícitamente, si es un contexto con un solo subproceso para usar (por ejemplo, un subproceso de interfaz de usuario). Por lo tanto, llama al método en un solo hilo y luego lo bloquea, esperando que se complete la operación. La descarga comienza a través de la red y espera su finalización. Por defecto, esperar una
Task
capturará el
SynchronizationContext
actual (y en este caso), y cuando se complete la descarga de la red, se pondrá en cola nuevamente en la devolución de llamada
SynchronizationContext
, que llamará al resto de la operación. Pero el único hilo que puede manejar la devolución de llamada en la cola está actualmente bloqueado mientras espera que se complete la operación. Y esta operación no se completará hasta que se procese la devolución de llamada. Punto muerto! Puede ocurrir incluso cuando el contexto no limita la concurrencia a 1, pero los recursos están limitados de alguna manera. Imagine la misma situación, solo con un valor de 4 para
MaxConcurrencySynchronizationContext
. En lugar de ejecutar la operación una vez, ponemos en cola 4 llamadas al contexto. Cada llamada se realiza y se bloquea antes de su finalización. Todos los recursos ahora están bloqueados esperando la finalización de los métodos asincrónicos, y lo único que les permitirá completar es si sus devoluciones de llamada son procesadas por este contexto. Sin embargo, él ya está completamente ocupado. Punto muerto de nuevo. Si el método de la biblioteca utilizara
ConfigureAwait(false)
lugar, no pondría en cola la devolución de llamada al contexto original, lo que evitaría los scripts de punto muerto.
¿Necesito usar ConfigureAwait (verdadero)?
No, a menos que necesite indicar explícitamente que no está utilizando
ConfigureAwait(false)
(por ejemplo, para ocultar advertencias de análisis estático, etc.).
ConfigureAwait(true)
no hace nada significativo. Si compara
await task
y
await task.ConfigureAwait(true)
, serán funcionalmente idénticos. Por lo tanto, si
ConfigureAwait(true)
presente en el código, se puede eliminar sin consecuencias negativas.
El método
ConfigureAwait
toma un valor booleano, ya que en algunas situaciones puede necesitar pasar una variable para controlar la configuración. Pero en el 99% de los casos, el valor se establece en falso,
ConfigureAwait(false)
.
¿Cuándo usar ConfigureAwait (falso)?
Depende de si implementa código de nivel de aplicación o código de biblioteca de propósito general.
Al escribir aplicaciones, generalmente se requiere algún comportamiento predeterminado. Si el modelo / entorno de la aplicación (por ejemplo, Windows Forms, WPF, ASP.NET Core) publica un
SynchronizationContext
especial, es casi seguro que hay una buena razón para esto: significa que el código le permite cuidar el contexto de sincronización para una interacción adecuada con el modelo / entorno de la aplicación. Por ejemplo, si escribe un controlador de eventos en una aplicación Windows Forms, una prueba en xUnit o un código en un controlador ASP.NET MVC, independientemente de si el modelo de la aplicación ha publicado un
SynchronizationContext
, debe usar
SynchronizationContext
si existe. Esto significa que si
await
ConfigureAwait(true)
y
await
, las devoluciones de llamada / continuaciones se envían de vuelta al contexto original; todo sale como debería. Desde aquí puede formular una regla general:
si escribe código a nivel de aplicación, no use ConfigureAwait(false)
. Volvamos al controlador de clics:
private static readonly HttpClient s_httpClient = new HttpClient(); private async void downloadBtn_Click(object sender, RoutedEventArgs e) { string text = await s_httpClient.GetStringAsync("http://example.com/currenttime"); downloadBtn.Content = text; }
downloadBtn.Content = text
debe ejecutarse en el contexto original. Si el código violó esta regla y usó
ConfigureAwait (false)
lugar, entonces no se usará en el contexto original:
private static readonly HttpClient s_httpClient = new HttpClient(); private async void downloadBtn_Click(object sender, RoutedEventArgs e) { string text = await s_httpClient.GetStringAsync("http://example.com/currenttime").ConfigureAwait(false);
Esto conducirá a un comportamiento inapropiado. Lo mismo se aplica al código en una aplicación ASP.NET clásica que depende de
HttpContext.Current
. Cuando se utiliza
ConfigureAwait(false)
es probable que
ConfigureAwait(false)
intento posterior de utilizar la función
Context.Current
problemas.
Esto es lo que distingue a las bibliotecas de uso general. Son universales en parte porque no les importa el entorno en el que se utilizan. Puede usarlos desde una aplicación web, desde una aplicación cliente o desde una prueba, no importa, ya que el código de la biblioteca es independiente del modelo de aplicación en el que se puede usar. Agnóstico también significa que la biblioteca no hará nada para interactuar con el modelo de aplicación, por ejemplo, no obtendrá acceso a los controles de la interfaz de usuario, porque la biblioteca de propósito general no sabe nada sobre ellos. Dado que no hay necesidad de ejecutar el código en ningún entorno en particular, podemos evitar forzar las continuaciones / devoluciones de llamada para que se vean obligadas al contexto original, y lo hacemos utilizando
ConfigureAwait(false)
, que nos brinda ventajas de rendimiento y aumenta la confiabilidad. Esto nos lleva a lo siguiente:
si está escribiendo un código de biblioteca de uso general, use ConfigureAwait(false)
. Esta es la razón por la cual todos (o casi todos) esperan en las bibliotecas de tiempo de ejecución de .NET Core usan ConfigureAwait (falso); Con algunas excepciones, que probablemente sean errores, se corregirán.
Por ejemplo, el PR corregido ninguna llamada ConfigureAwait(false)
en HttpClient
.Por supuesto, esto no tiene sentido en todas partes. Por ejemplo, una de las grandes excepciones (o al menos casos en los que necesita pensarlo) en las bibliotecas de uso general es cuando estas bibliotecas tienen API que aceptan delegados a una llamada. En tales casos, la biblioteca acepta código potencial de nivel de aplicación de la persona que llama, lo que hace que estas suposiciones para la biblioteca de propósito general sean muy controvertidas. Imagine, por ejemplo, la versión asincrónica del método Where LINQ: public static async IAsyncEnumerable<T> WhereAsync(this IAsyncEnumerable<T> source, Func<T, bool> predicate)
¿Debería predicate
llamarse en la fuente del código de SynchronizationContext
llamada? WhereAsync
, y esta es la razón por la que puede decidir no usar ConfigureAwait(false)
.Incluso en casos especiales, siga la recomendación general: utilícela ConfigureAwait(false)
si está escribiendo un código agnóstico de biblioteca / aplicación-modelo-propósito general.¿ConfigureAwait (falso) garantiza que la devolución de llamada no se ejecutará en el contexto original?
No, esto garantiza que no se colocará en la cola del contexto original. Pero esto no significa que el código posterior await
no se ejecutará en el contexto original. Esto se debe al hecho de que las operaciones ya completadas se devuelven sincrónicamente y no se devuelven forzosamente a la cola. Por lo tanto, si espera una tarea que ya se completó en el momento en que espera, independientemente de si la está utilizando ConfigureAwait(false)
, el código inmediatamente posterior continuará ejecutándose en el hilo actual en un contexto que todavía es válido.ConfigureAwait (false) , — ?
En general, no. Recuerda las preguntas frecuentes anteriores. Si await task.ConfigureAwait(false)
incluye una tarea que ya se completó en el momento de la espera (que en realidad sucede con bastante frecuencia), el uso no ConfigureAwait(false)
tendrá sentido, ya que el hilo continúa ejecutando el siguiente código en el método y todavía está en el mismo contexto que antes.Una excepción notable es que la primera await
siempre terminará de forma asincrónica, y la operación esperada lo devolverá en un entorno libre de especiales SynchronizationContext
o TaskScheduler
. Por ejemplo, CryptoStream
en las bibliotecas de tiempo de ejecución, .NET verifica que su código potencialmente computacionalmente intensivo no se ejecute como parte de una invocación síncrona del código de llamada. Para hacer esto, usa un especialawaiter
para asegurarse de que el código después de la primera espera se ejecute en el subproceso del grupo de subprocesos. Sin embargo, incluso en este caso, notará que la próxima espera todavía está en uso ConfigureAwait(false)
; Técnicamente, esto no es necesario, pero simplifica enormemente la revisión del código, ya que no es necesario entender por qué no se usó ConfigureAwait(false)
.¿Es posible usar Task.Run para evitar usar ConfigureAwait (falso)?
Si, si escribes: Task.Run(async delegate { await SomethingAsync();
entonces ConfigureAwait(false)
in SomethingAsync()
será superfluo, ya que el delegado pasado Task.Run
se ejecutará en el subproceso del grupo de subprocesos, de modo que sin cambiar el código anterior, SynchronizationContext.Current
devolverá un valor null
. Además, Task.Run
utiliza implícitamente TaskScheduler.Default
, por lo tanto, TaskScheduler.Current
dentro del delegado también devolverá un valor Default
. Esto significa que await
tendrá el mismo comportamiento independientemente de si se utilizó ConfigureAwait(false)
. Tampoco puede garantizar lo que puede hacer el código dentro de este lambda. Si tienes un código: Task.Run(async delegate { SynchronizationContext.SetSynchronizationContext(new SomeCoolSyncCtx()); await SomethingAsync();
entonces el código dentro SomethingAsync
verá realmente la SynchronizationContext.Current
instancia SomeCoolSyncCtx
. y esto await
, y cualquier expectativa no configurada dentro de SomethingAsync se devolverá a este contexto. Por lo tanto, para utilizar este enfoque, es necesario comprender qué puede hacer o no todo el código que coloca en la cola, y si sus acciones pueden convertirse en un obstáculo.Este enfoque también ocurre debido a la necesidad de crear / poner en cola un objeto de tarea adicional. Esto puede o no importar a la aplicación / biblioteca, dependiendo de los requisitos de rendimiento.También tenga en cuenta que tales soluciones pueden causar más problemas que beneficios y tener diferentes consecuencias no deseadas. Por ejemplo, algunas herramientas de análisis estático ConfigureAwait(false)
señalan expectativas que no usan CA2007 . Si enciende el analizador y luego usa un truco para evitar su uso ConfigureAwait
, existe una alta probabilidad de que el analizador lo marque. Esto puede implicar aún más trabajo, por ejemplo, es posible que desee deshabilitar el analizador debido a su importancia, y esto implicará omitir otros lugares en la base del código donde realmente necesita usarlo ConfigureAwait(false)
.¿Es posible usar SynchronizationContext.SetSynchronizationContext para evitar usar ConfigureAwait (false)?
No
Aunque es posible. Depende de la implementación utilizada.Algunos desarrolladores hacen esto: Task t; SynchronizationContext old = SynchronizationContext.Current; SynchronizationContext.SetSynchronizationContext(null); try { t = CallCodeThatUsesAwaitAsync();
con la esperanza de que esto obligue al código interno a CallCodeThatUsesAwaitAsync
ver el contexto actual como null
. Así será. Sin embargo, esta opción no afectará a cuál await
ve TaskScheduler.Current
. Por lo tanto, si el código se ejecuta en un especial TaskScheduler
, su await
interior CallCodeThatUsesAwaitAsync
verá y hará cola para ese especial TaskScheduler
.Al igual que en las Task.Run
preguntas frecuentes, las mismas advertencias se aplican aquí: hay ciertas consecuencias de este enfoque, y el código dentro del bloque try
también puede interferir con estos intentos al establecer un contexto diferente (o llamar al código usando un programador de tareas no estándar).Con esta plantilla, también debe tener cuidado con los cambios menores: SynchronizationContext old = SynchronizationContext.Current; SynchronizationContext.SetSynchronizationContext(null); try { await t; } finally { SynchronizationContext.SetSynchronizationContext(old); }
¿Ves cuál es el problema? Un poco difícil de notar, pero es impresionante. No hay garantía de que la espera eventualmente cause una devolución de llamada / continúe en el hilo original. Esto significa que el retorno SynchronizationContext
al original puede no ocurrir en el hilo original, lo que puede llevar al hecho de que los elementos de trabajo posteriores en este hilo verán el contexto incorrecto. Para contrarrestar esto, los modelos de aplicación bien escritos que especifican un contexto especial suelen agregar código para restablecerlo manualmente antes de llamar a cualquier código personalizado adicional. E incluso si esto sucede en un subproceso, puede llevar un tiempo durante el cual el contexto puede no restaurarse correctamente. Y si funciona en un hilo diferente, esto puede conducir a la instalación del contexto incorrecto. Y así sucesivamente.
Bastante lejos del ideal.¿Debo usar ConfigureAwait (false) si uso GetAwaiter () .GetResult ()?
No
ConfigureAwait
afecta solo las devoluciones de llamada. En particular, la plantilla awaiter
requiere que awaiter
proporcione la propiedad IsCompleted
, los métodos GetResult
y OnCompleted
(opcionalmente con el método UnsafeOnCompleted). ConfigureAwait
solo afecta el comportamiento {Unsafe}OnCompleted
, por lo que si llama directamente GetResult()
, independientemente de si lo hace TaskAwaiter
o no ConfiguredTaskAwaitable.ConfiguredTaskAwaiter
hay diferencia en el comportamiento. Por lo tanto, si ve task.ConfigureAwait(false).GetAwaiter().GetResult()
que puede reemplazarlo con task.GetAwaiter().GetResult()
(además, piense si realmente necesita dicha implementación).Sé que el código se ejecuta en un entorno en el que nunca habrá un SynchronizationContext especial o un TaskScheduler especial. ¿No puedo usar ConfigureAwait (falso)?
Posiblemente
Depende de qué tan seguro esté de "nunca". Como se mencionó en las preguntas anteriores, solo porque el modelo de la aplicación en la que está trabajando no especifica uno especial SynchronizationContext
y no llama a su código en TaskScheduler
uno especial no significa que el código de otro usuario o biblioteca no los use. Por lo tanto, debe estar seguro de esto, o al menos reconocer el riesgo de que tal opción sea posible.Escuché que en .NET Core no hay necesidad de aplicar ConfigureAwait (falso). Es asi?
No asi.
Es necesario cuando se trabaja en .NET Core por las mismas razones que cuando se trabaja en .NET Framework. Nada ha cambiado a este respecto.Ha cambiado si ciertos entornos publican los suyos SynchronizationContext
. En particular, mientras que ASP.NET clásico en .NET Framework tiene el suyo SynchronizationContext
, ASP.NET Core no lo tiene. Esto significa que el código que se ejecuta en la aplicación ASP.NET Core no verá un código especial de forma predeterminada SynchronizationContext
, lo que reduce la necesidad ConfigureAwait(false)
de este entorno.Sin embargo, esto no significa que nunca habrá una costumbre SynchronizationContext
oTaskScheduler
. Si algún código de usuario (u otro código de biblioteca utilizado por la aplicación) establece el contexto del usuario y llama a su código o llama a su código en la Tarea programada en el programador de tareas especial, entonces await
ASP.NET Core verá un contexto o programador no estándar, lo cual puede requerir uso ConfigureAwait(false)
. Por supuesto, en situaciones en las que evita los bloqueos sincrónicos (lo que debe hacer en las aplicaciones web de todos modos) y si no está en contra de la pequeña sobrecarga de rendimiento en algunos casos, puede hacerlo sin usar ConfigureAwait(false)
.¿Puedo usar ConfigureAwait cuando "espero a que foreach se complete" en IAsyncEnumerable?
Si
Consulte el artículo de MSDN para ver un ejemplo .Await foreach
coincide con el patrón y, por lo tanto, se puede usar para enumerar IAsyncEnumerable<T>
. También se puede usar para enumerar elementos que representan el alcance correcto de la API. bibliotecas de ejecución .NET incluyen un método de expansión ConfigureAwait
para IAsyncEnumerable<T>
que devuelve un tipo especial, que se envuelve IAsyncEnumerable<T>
y Boolean
se corresponde con la plantilla correcta. Cuando el compilador genera llamadas MoveNextAsync
y al DisposeAsync
enumerador. Estas llamadas están relacionadas con el tipo de estructura de enumerador configurado devuelto, que a su vez cumple las expectativas según sea necesario.¿Puedo usar ConfigureAwait con 'await using' IAsyncDisposable?
Sí, aunque con una pequeña complicación.Al igual que con IAsyncEnumerable<T>
, biblioteca .NET de tiempo de ejecución proporciona un método de extensión ConfigureAwait
de IAsyncDisposable
, y await using
funciona muy bien, ya que implementa la plantilla adecuada (es decir, proporciona un método correspondiente DisposeAsync
): await using (var c = new MyAsyncDisposableClass().ConfigureAwait(false)) { ... }
El problema aquí es que el tipo c
ahora no lo es MyAsyncDisposableClass
, sino que es el System.Runtime.CompilerServices.ConfiguredAsyncDisposable
que devuelve el método de extensión ConfigureAwait
para IAsyncDisposable
.Para evitar esto, agregue la línea: var c = new MyAsyncDisposableClass(); await using (c.ConfigureAwait(false)) { ... }
Ahora el tipo es c
nuevamente deseado MyAsyncDisposableClass
. Lo que también tiene el efecto de aumentar el alcance de c
; si es necesario, puede envolverlo todo entre llaves.Usé ConfigureAwait (falso), pero mi AsyncLocal todavía fluía en el código después de esperar. ¿Es esto un error?
No, esto es bastante esperado. El flujo de datos AsyncLocal<T>
es una parte ExecutionContext
separada de SynchronizationContext
. Si no lo hace de forma explícita corriente desconectada ExecutionContext
utilizando ExecutionContext.SuppressFlow()
, ExecutionContext
(y por lo tanto los datos AsyncLocal <T>
) siempre va a pasar a través awaits
, independientemente de si se utiliza ConfigureAwait
con el fin de evitar la captura de la fuente SynchronizationContext
. Más detalles se discuten en este artículo .¿Pueden las herramientas de lenguaje ayudarme a evitar la necesidad de usar explícitamente ConfigureAwait (falso) en mi biblioteca?
Los desarrolladores de bibliotecas a veces se quejan de la necesidad de usar ConfigureAwait(false)
y piden alternativas menos invasivas.Actualmente no lo son, al menos no están integrados en el lenguaje / compilador / tiempo de ejecución. Sin embargo, hay muchas sugerencias sobre cómo se puede implementar esto, por ejemplo: 1 , 2 , 3 , 4 .Si el tema que le interesa, si tiene ideas nuevas e interesantes, el autor del artículo original lo invita a una discusión.