practices method false example configureawait best await async c# async-await task-parallel-library task synchronizationcontext

c# - method - ¿Deberíamos usar ConfigureAwait(falso) en las bibliotecas que llaman devoluciones de llamada asincrónicas?



configureawait(false) c# (3)

La pregunta es, ¿deberíamos usar ConfigureAwait (falso) en este caso?

Si deberías. Si la Task interna que se está esperando tiene en cuenta el contexto y utiliza un contexto de sincronización dado, aún podrá capturarlo incluso si quien lo invoca usa ConfigureAwait(false) . No olvide que al ignorar el contexto, lo hace en la llamada de nivel superior, no dentro del delegado proporcionado. El delegado que se está ejecutando dentro de la Task , si es necesario, deberá tener en cuenta el contexto.

Usted, el invocador, no tiene interés en el contexto, por lo que está absolutamente bien invocarlo con ConfigureAwait(false) . Esto efectivamente hace lo que usted quiere, deja la opción de si el delegado interno incluirá el contexto de sincronización hasta el llamador de su método de Map .

Editar:

Lo importante a tener en cuenta es que una vez que use ConfigureAwait(false) , cualquier método que se ejecute después de eso se activará en un subproceso de subprocesos arbitrario.

Una buena idea sugerida por @ i3arnon sería aceptar un indicador bool opcional que indique si el contexto es necesario o no. Aunque un poco feo, sería un buen trabajo.

Hay muchas pautas para cuándo usar ConfigureAwait(false) , cuando se usa await / async en C #.

Parece que la recomendación general es usar ConfigureAwait(false) en el código de la biblioteca, ya que rara vez depende del contexto de sincronización.

Sin embargo, supongamos que estamos escribiendo un código de utilidad muy genérico, que toma una función como entrada. Un ejemplo simple podría ser los siguientes combinadores funcionales (incompletos), para facilitar las operaciones simples basadas en tareas:

Mapa:

public static async Task<TResult> Map<T, TResult>(this Task<T> task, Func<T, TResult> mapping) { return mapping(await task); }

Mapa plano:

public static async Task<TResult> FlatMap<T, TResult>(this Task<T> task, Func<T, Task<TResult>> mapping) { return await mapping(await task); }

La pregunta es, ¿deberíamos usar ConfigureAwait(false) en este caso? No estoy seguro de cómo funciona la captura de contexto wrt. cierres

Por un lado, si los combinadores se utilizan de manera funcional, el contexto de sincronización no debería ser necesario. Por otro lado, las personas pueden hacer un mal uso de la API y hacer cosas dependientes del contexto en las funciones proporcionadas.

Una opción sería tener métodos separados para cada escenario ( Map y MapWithContextCapture o algo así), pero se siente feo.

Otra opción podría ser agregar la opción de asignar / mapa plano desde y hacia un ConfiguredTaskAwaitable<T> , pero como los awaitables no tienen que implementar una interfaz, esto generaría un gran número de código redundante y, en mi opinión, sería aún peor.

¿Hay una buena manera de cambiar la responsabilidad a la persona que llama, de modo que la biblioteca implementada no tenga que hacer suposiciones sobre si el contexto es necesario o no en las funciones de mapeo provistas?

¿O es simplemente un hecho, que los métodos asíncronos no se componen demasiado bien, sin varias suposiciones?

EDITAR

Solo para aclarar algunas cosas:

  1. El problema existe. Cuando ejecuta la "devolución de llamada" dentro de la función de utilidad, la adición de ConfigureAwait(false) resultará en una sincronización nula. contexto.
  2. La pregunta principal es cómo debemos abordar la situación. Deberíamos ignorar el hecho de que alguien podría querer usar la sincronización. contexto, o hay una buena manera de transferir la responsabilidad a la persona que llama, además de agregar alguna sobrecarga, marca o similar?

Como mencionan algunas respuestas, sería posible agregar un bool-flag al método, pero como yo lo veo, tampoco es demasiado bonito, ya que tendrá que propagarse a través de las API (ya que más funciones de "utilidad", dependiendo de las que se muestran arriba).


Creo que el verdadero problema aquí proviene del hecho de que está agregando operaciones a la Task mientras realmente opera con el resultado de la misma.

No hay ninguna razón real para duplicar estas operaciones para la tarea como un contenedor en lugar de mantenerlas en el resultado de la tarea.

De esa manera, no necesita decidir cómo await esta tarea en un método de utilidad ya que esa decisión permanece en el código del consumidor.

Si Map se implementa en su lugar de la siguiente manera:

public static TResult Map<T, TResult>(this T value, Func<T, TResult> mapping) { return mapping(value); }

Puedes usarlo fácilmente con o sin Task.ConfigureAwait consecuencia:

var result = await task.ConfigureAwait(false) var mapped = result.Map(result => Foo(result));

Map aquí es sólo un ejemplo. El punto es qué estás manipulando aquí. Si está manipulando la tarea, no debe await y pasar el resultado a un delegado de consumidor, simplemente puede agregar alguna lógica async y la persona que llama puede elegir si usar Task.ConfigureAwait o no. Si está trabajando en el resultado, no tiene que preocuparse por una tarea.

Puede pasar un valor booleano a cada uno de estos métodos para indicar si desea continuar en el contexto capturado o no (o incluso aprobar de manera más robusta un enum indicadores de opciones para admitir otras configuraciones en await ). Pero eso viola la separación de preocupaciones, ya que esto no tiene nada que ver con el Map (o su equivalente).


Cuando dice await task.ConfigureAwait(false) transición al grupo de subprocesos haciendo que la mapping ejecute en un contexto nulo en lugar de ejecutarse en el contexto anterior. Eso puede causar diferentes comportamientos. Así que si la persona que llama escribió:

await Map(0, i => { myTextBox.Text = i.ToString(); return 0; }); //contrived...

Entonces esto se bloquearía bajo la siguiente implementación del Map :

var result = await task.ConfigureAwait(false); return await mapper(result);

Pero no aquí:

var result = await task/*.ConfigureAwait(false)*/; ...

Aún más horrible:

var result = await task.ConfigureAwait(new Random().Next() % 2 == 0); ...

¡Lanza una moneda sobre el contexto de sincronización! Esto parece divertido, pero no es tan absurdo como parece. Un ejemplo más realista sería:

var result = someConfigFlag ? await GetSomeValue<T>() : await task.ConfigureAwait(false);

De modo que dependiendo de algún estado externo, el contexto de sincronización bajo el cual se ejecuta el resto del método puede cambiar.

Esta es una debilidad del diseño de await .

El problema más desconcertante aquí es que cuando se llama a la API no está claro qué sucede. Esto es confuso y causa errores. Por lo tanto, es mejor garantizar un comportamiento determinista siempre utilizando task.ConfigureAwait(false) .

La lambda debe asegurarse de que funcione en el contexto correcto:

var uiScheduler = TaskScheduler.FromCurrentSynchronizationContext; Map(..., async x => await Task.Factory.StartNew( () => { /*access UI*/ }, CancellationToken.None, TaskCreationOptions.None, uiScheduler));

Probablemente sea mejor ocultar algo de esto en un método de utilidad.

Esta no es una solución elegante, pero creo que es la menos mala de las opciones posibles.

Alternativamente, puede inyectar un parámetro booleano en Map que especifique si fluye el contexto o no. Eso haría explícito el comportamiento. Este es un diseño sólido de API pero desordena la API. Parece inapropiado preocuparse por una API básica como Map with synchronization context issues.