tasks parallel method example ejemplos create await async c# .net task-parallel-library async-await

parallel - task c# example



Revisiting Task.ConfigureAwait(continueOnCapturedContext: false) (2)

El objetivo principal del diseño detrás de ConfigureAwait (falso) es reducir el SynchronizationContext redundante. Poste las devoluciones de llamada de continuación para esperar, siempre que sea posible. Esto generalmente significa menos cambio de hilo y menos trabajo en los hilos de la interfaz de usuario.

No estoy de acuerdo con tu premisa. ConfigureAwait(false) objetivo de ConfigureAwait(false) es reducir, en la medida de lo posible, el trabajo que se debe volver a ordenar a contextos "especiales" (por ejemplo, UI) a pesar de los cambios de subprocesos que puede requerir fuera de ese contexto.

Si el objetivo era reducir los cambios de subprocesos, podría permanecer en el mismo contexto especial durante todo el trabajo, y luego no se requieren otros subprocesos.

Para lograrlo, debe usar ConfigureAwait todas partes donde no le importe que el hilo ejecute la continuación. Si tomas tu ejemplo y usas ConfigureAwait apropiadamente, solo obtendrás un solo interruptor (en lugar de 2 sin él):

private async void Button_Click(object sender, RoutedEventArgs e) { TaskExt.Log("A1"); await AnotherClass.MethodAsync().ConfigureAwait(false); TaskExt.Log("A2"); } public class AnotherClass { public static async Task MethodAsync() { TaskExt.Log("B1"); await SomeClass.SomeAsyncApi().ConfigureAwait(false); TaskExt.Log("B2"); } } public class SomeClass { public static async Task<int> SomeAsyncApi() { TaskExt.Log("X1"); await Task.Delay(1000).WithCompletionLog(step: "X1.5").ConfigureAwait(false); TaskExt.Log("X2"); return 42; } }

Salida:

{ step = A1, thread = 9 } { step = B1, thread = 9 } { step = X1, thread = 9 } { step = X1.5, thread = 11 } { step = X2, thread = 11 } { step = B2, thread = 11 } { step = A2, thread = 11 }

Ahora, si te importa el hilo de la continuación (por ejemplo, cuando usas controles de UI) "pagas" cambiando a ese hilo, publicando el trabajo relevante en ese hilo. Aún has ganado de todo el trabajo que no requirió ese hilo.

Si desea llevarlo aún más lejos y eliminar el trabajo sincrónico de estos métodos async del hilo de la interfaz de usuario, solo necesita usar Task.Run una vez y agregar otro interruptor:

private async void Button_Click(object sender, RoutedEventArgs e) { TaskExt.Log("A1"); await Task.Run(() => AnotherClass.MethodAsync()).ConfigureAwait(false); TaskExt.Log("A2"); }

Salida:

{ step = A1, thread = 9 } { step = B1, thread = 10 } { step = X1, thread = 10 } { step = X1.5, thread = 11 } { step = X2, thread = 11 } { step = B2, thread = 11 } { step = A2, thread = 11 }

Esta guía para usar ConfigureAwait(false) está dirigida a los desarrolladores de bibliotecas porque allí es donde realmente importa, pero el punto es usarla siempre que pueda y, en ese caso, reduce el trabajo en estos contextos especiales mientras mantiene el cambio de hilo al mínimo.

Usar WithNoContext tiene exactamente el mismo resultado que usar ConfigureAwait(false) todas partes. Sin embargo, la desventaja es que se mete con el SynchronizationContext del hilo y que no eres consciente de eso dentro del método async . ConfigureAwait afecta directamente la await actual para que tenga la causa y el efecto juntos.

El uso de Task.Run también, como he señalado, tiene exactamente el mismo resultado que el uso de ConfigureAwait(false) todas partes con el valor agregado de descargar las partes síncronas del método async en ThreadPool . Si esto es necesario, entonces Task.Run es apropiado; de lo contrario, ConfigureAwait(false) es suficiente.

Ahora, si está lidiando con una biblioteca con errores cuando ConfigureAwait(false) no se usa adecuadamente, puede hackearlo eliminando el SynchronizationContext pero usando Thread.Run es mucho más simple y claro y descargar el trabajo en ThreadPool tiene un muy insignificante sobrecarga.

Demasiado largo para leer. El uso de Task.ConfigureAwait(continueOnCapturedContext: false) puede estar introduciendo un cambio de hilo redundante. Estoy buscando una solución consistente para eso.

Versión larga. El objetivo principal del diseño detrás de ConfigureAwait(false) es reducir el SynchronizationContext.Post redundante. SynchronizationContext.Post devoluciones de llamada de continuación para await , siempre que sea posible. Esto generalmente significa menos cambio de hilo y menos trabajo en los hilos de la interfaz de usuario. Sin embargo, no siempre es así como funciona.

Por ejemplo, hay una biblioteca de terceros que implementa la API SomeAsyncApi . Tenga en cuenta que ConfigureAwait(false) no se usa en ninguna parte de esta biblioteca, por alguna razón:

// some library, SomeClass class public static async Task<int> SomeAsyncApi() { TaskExt.Log("X1"); // await Task.Delay(1000) without ConfigureAwait(false); // WithCompletionLog only shows the actual Task.Delay completion thread // and doesn''t change the awaiter behavior await Task.Delay(1000).WithCompletionLog(step: "X1.5"); TaskExt.Log("X2"); return 42; } // logging helpers public static partial class TaskExt { public static void Log(string step) { Debug.WriteLine(new { step, thread = Environment.CurrentManagedThreadId }); } public static Task WithCompletionLog(this Task anteTask, string step) { return anteTask.ContinueWith( _ => Log(step), CancellationToken.None, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default); } }

Ahora, digamos que hay un código de cliente ejecutándose en un subproceso de interfaz de usuario de WinForms y usando SomeAsyncApi :

// another library, AnotherClass class public static async Task MethodAsync() { TaskExt.Log("B1"); await SomeClass.SomeAsyncApi().ConfigureAwait(false); TaskExt.Log("B2"); } // ... // a WinFroms app private async void Form1_Load(object sender, EventArgs e) { TaskExt.Log("A1"); await AnotherClass.MethodAsync(); TaskExt.Log("A2"); }

La salida:

{ step = A1, thread = 9 } { step = B1, thread = 9 } { step = X1, thread = 9 } { step = X1.5, thread = 11 } { step = X2, thread = 9 } { step = B2, thread = 11 } { step = A2, thread = 9 }

Aquí, el flujo de ejecución lógica pasa por 4 conmutadores de hilo. 2 de ellos son redundantes y causados ​​por SomeAsyncApi().ConfigureAwait(false) . Ocurre porque ConfigureAwait(false) empuja la continuación a ThreadPool desde un subproceso con contexto de sincronización (en este caso, el subproceso de la interfaz de usuario).

En este caso particular, MethodAsync está mejor sin ConfigureAwait(false) . Entonces solo se necesitan 2 conmutadores de hilo vs 4:

{ step = A1, thread = 9 } { step = B1, thread = 9 } { step = X1, thread = 9 } { step = X1.5, thread = 11 } { step = X2, thread = 9 } { step = B2, thread = 9 } { step = A2, thread = 9 }

Sin embargo, la autora de MethodAsync usa ConfigureAwait(false) con todas las buenas intenciones y siguiendo las mejores prácticas , y no sabe nada sobre la implementación interna de SomeAsyncApi . No sería un problema si ConfigureAwait(false) se usara "completamente" (es decir, también dentro de SomeAsyncApi ), pero eso está fuera de su control.

Así es como sucede con WindowsFormsSynchronizationContext (o DispatcherSynchronizationContext ), donde es posible que no nos importe en absoluto los cambios de subprocesos adicionales. Sin embargo, una situación similar podría ocurrir en ASP.NET, donde AspNetSynchronizationContext.Post esencialmente hace esto:

Task newTask = _lastScheduledTask.ContinueWith(_ => SafeWrapCallback(action)); _lastScheduledTask = newTask;

Todo esto puede parecer un problema artificial, pero vi mucho código de producción como este, tanto del lado del cliente como del lado del servidor. Otro patrón cuestionable que encontré: await TaskCompletionSource.Task.ConfigureAwait(false) con SetResult llamado en el mismo contexto de sincronización que el capturado para el primero. Nuevamente, la continuación fue empujada de forma redundante a ThreadPool . El razonamiento detrás de este patrón fue que "ayuda a evitar puntos muertos".

La pregunta : a la luz del comportamiento descrito de ConfigureAwait(false) , estoy buscando una forma elegante de usar async/await mientras sigo minimizando el cambio redundante de hilo / contexto. Idealmente, algo que funcione con las bibliotecas de terceros existentes.

Lo que he visto hasta ahora :

  • Descargar un lambda async con Task.Run no es ideal, ya que introduce al menos un interruptor de hilo adicional (aunque potencialmente puede salvar muchos otros):

    await Task.Run(() => SomeAsyncApi()).ConfigureAwait(false);

  • Otra solución pirata podría ser eliminar temporalmente el contexto de sincronización del hilo actual, por lo que no será capturado por ninguna espera posterior en la cadena interna de llamadas (lo mencioné anteriormente here ):

    async Task MethodAsync() { TaskExt.Log("B1"); await TaskExt.WithNoContext(() => SomeAsyncApi()).ConfigureAwait(false); TaskExt.Log("B2"); }

    { step = A1, thread = 8 } { step = B1, thread = 8 } { step = X1, thread = 8 } { step = X1.5, thread = 10 } { step = X2, thread = 10 } { step = B2, thread = 10 } { step = A2, thread = 8 }

    public static Task<TResult> WithNoContext<TResult>(Func<Task<TResult>> func) { Task<TResult> task; var sc = SynchronizationContext.Current; try { SynchronizationContext.SetSynchronizationContext(null); // do not await the task here, so the SC is restored right after // the execution point hits the first await inside func task = func(); } finally { SynchronizationContext.SetSynchronizationContext(sc); } return task; }

    Esto funciona, pero no me gusta el hecho de que altere el contexto de sincronización actual del hilo, aunque sea por un alcance muy corto. Además, hay otra implicación aquí: en ausencia de SynchronizationContext en el subproceso actual, se utilizará un TaskScheduler.Current ambiental para await continuaciones. Para tener en cuenta esto, WithNoContext podría modificarse como se muestra a continuación, lo que haría que este truco sea aún más exótico:

    // task = func(); var task2 = new Task<Task<TResult>>(() => func()); task2.RunSynchronously(TaskScheduler.Default); task = task2.Unwrap();

Agradecería cualquier otra idea.

Actualizado , para abordar el comentario de @ i3arnon :

Diría que es al revés porque, como dijo Stephen en su respuesta "El propósito de ConfigureAwait (falso) no es inducir un cambio de hilo (si es necesario), sino más bien evitar que se ejecute demasiado código en un contexto especial en particular. " con el que no está de acuerdo y es la raíz de su cumplimiento.

Como su respuesta ha sido editada, aquí está su declaración con la que no estoy de acuerdo, para mayor claridad:

El objetivo de ConfigureAwait (falso) es reducir, en la medida de lo posible, el trabajo que los subprocesos "especiales" (por ejemplo, UI) deben procesar a pesar de los conmutadores de subprocesos que requiere.

Tampoco estoy de acuerdo con su versión actual de esa declaración. Te referiré a la fuente principal, la publicación de blog de Stephen Toub:

Evite el innecesario Marshaling

Si es posible, asegúrese de que la implementación asíncrona que está llamando no necesita el subproceso bloqueado para completar la operación (de esa manera, puede usar mecanismos de bloqueo normales para esperar sincrónicamente a que el trabajo asincrónico se complete en otro lugar). En el caso de asíncrono / espera, esto generalmente significa asegurarse de que cualquier espera dentro de la implementación asincrónica que está llamando esté utilizando ConfigureAwait (falso) en todos los puntos de espera; esto evitará que la espera intente volver a ordenar el SynchronizationContext actual. Como implementador de la biblioteca, es una buena práctica usar siempre ConfigureAwait (falso) en todas sus esperas, a menos que tenga una razón específica para no hacerlo; Esto es bueno no solo para ayudar a evitar este tipo de problemas de punto muerto, sino también para el rendimiento, ya que evita costos de cálculo innecesarios.

Sí dice que el objetivo es evitar costos innecesarios de cálculo, para el rendimiento . Un cambio de subproceso (que fluye el ExecutionContext , entre otras cosas) es un gran costo de cálculo.

Ahora, no dice en ninguna parte que el objetivo es reducir la cantidad de trabajo que se realiza en hilos o contextos "especiales".

Si bien esto puede tener cierto sentido para los subprocesos de la interfaz de usuario, todavía no creo que sea el objetivo principal de ConfigureAwait . Hay otras formas, más estructuradas, de minimizar el trabajo en subprocesos de la interfaz de usuario, como el uso de fragmentos de await Task.Run(work) .

Además, no tiene ningún sentido minimizar el trabajo en AspNetSynchronizationContext , que fluye de hilo a hilo, a diferencia de un hilo de interfaz de usuario. Muy al contrario, una vez que esté en AspNetSynchronizationContext , desea hacer el mayor trabajo posible , para evitar cambios innecesarios en el medio del manejo de la solicitud HTTP. Sin embargo, todavía tiene mucho sentido usar ConfigureAwait(false) en ASP.NET: si se usa correctamente, nuevamente reduce la conmutación de hilos del lado del servidor.


Cuando se trata de operaciones asincrónicas, la sobrecarga de un conmutador de subproceso es demasiado pequeña para preocuparse (en términos generales). El propósito de ConfigureAwait(false) no es inducir un cambio de subproceso (si es necesario), sino más bien evitar que se ejecute demasiado código en un contexto especial en particular.

El razonamiento detrás de este patrón fue que "ayuda a evitar puntos muertos".

Y apilar inmersiones.

Pero sí creo que esto no es un problema en el caso general. Cuando encuentro un código que no usa ConfigureAwait correctamente, simplemente lo Task.Run en una Task.Run y Task.Run adelante. No vale la pena preocuparse por la sobrecarga de los conmutadores de hilo.