ConfigureAwait: Preguntas frecuentes

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

imagen

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); //  downloadBtn.Content = text; } 

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 predicatellamarse en la fuente del código de SynchronizationContextllamada? 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 awaitno 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 awaitsiempre terminará de forma asincrónica, y la operación esperada lo devolverá en un entorno libre de especiales SynchronizationContexto TaskScheduler. Por ejemplo, CryptoStreamen 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 especialawaiterpara 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.Runse ejecutará en el subproceso del grupo de subprocesos, de modo que sin cambiar el código anterior, SynchronizationContext.Currentdevolverá un valor null. Además, Task.Runutiliza implícitamente TaskScheduler.Default, por lo tanto, TaskScheduler.Currentdentro del delegado también devolverá un valor Default. Esto significa que awaittendrá 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(); //    SomeCoolSyncCtx }); 

entonces el código dentro SomethingAsyncverá realmente la SynchronizationContext.Currentinstancia 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)?


NoAunque es posible. Depende de la implementación utilizada.

Algunos desarrolladores hacen esto:

 Task t; SynchronizationContext old = SynchronizationContext.Current; SynchronizationContext.SetSynchronizationContext(null); try { t = CallCodeThatUsesAwaitAsync(); // await'      } finally { SynchronizationContext.SetSynchronizationContext(old); } await t; //  -     


con la esperanza de que esto obligue al código interno a CallCodeThatUsesAwaitAsyncver el contexto actual como null. Así será. Sin embargo, esta opción no afectará a cuál awaitve TaskScheduler.Current. Por lo tanto, si el código se ejecuta en un especial TaskScheduler, su awaitinterior CallCodeThatUsesAwaitAsyncverá y hará cola para ese especial TaskScheduler.

Al igual que en las Task.Runpreguntas frecuentes, las mismas advertencias se aplican aquí: hay ciertas consecuencias de este enfoque, y el código dentro del bloque trytambié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 SynchronizationContextal 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 ConfigureAwaitafecta solo las devoluciones de llamada. En particular, la plantilla awaiterrequiere que awaiterproporcione la propiedad IsCompleted, los métodos GetResulty OnCompleted(opcionalmente con el método UnsafeOnCompleted). ConfigureAwaitsolo afecta el comportamiento {Unsafe}OnCompleted, por lo que si llama directamente GetResult(), independientemente de si lo hace TaskAwaitero no ConfiguredTaskAwaitable.ConfiguredTaskAwaiterhay 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)?


PosiblementeDepende 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 SynchronizationContexty no llama a su código en TaskScheduleruno 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 SynchronizationContextoTaskScheduler. 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 awaitASP.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?


SiConsulte el artículo de MSDN para ver un ejemplo .

Await foreachcoincide 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 Booleanse corresponde con la plantilla correcta. Cuando el compilador genera llamadas MoveNextAsyncy al DisposeAsyncenumerador. 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 ConfigureAwaitde IAsyncDisposable, y await usingfunciona 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 cahora no lo es MyAsyncDisposableClass, sino que es el System.Runtime.CompilerServices.ConfiguredAsyncDisposableque devuelve el método de extensión ConfigureAwaitpara IAsyncDisposable.

Para evitar esto, agregue la línea:

 var c = new MyAsyncDisposableClass(); await using (c.ConfigureAwait(false)) { ... } 

Ahora el tipo es cnuevamente 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 ExecutionContextseparada de SynchronizationContext. Si no lo hace de forma explícita corriente desconectada ExecutionContextutilizando ExecutionContext.SuppressFlow(), ExecutionContext(y por lo tanto los datos AsyncLocal <T>) siempre va a pasar a través awaits, independientemente de si se utiliza ConfigureAwaitcon 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.

Source: https://habr.com/ru/post/482354/


All Articles