ConfigureAwait empuja la continuación a un hilo de grupo

Aquí hay un código de WinForms:

async void Form1_Load(object sender, EventArgs e) { // on the UI thread Debug.WriteLine(new { where = "before", Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.IsThreadPoolThread }); var tcs = new TaskCompletionSource(); this.BeginInvoke(new MethodInvoker(() => tcs.SetResult(true))); await tcs.Task.ContinueWith(t => { // still on the UI thread Debug.WriteLine(new { where = "ContinueWith", Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.IsThreadPoolThread }); }, TaskContinuationOptions.ExecuteSynchronously).ConfigureAwait(false); // on a pool thread Debug.WriteLine(new { where = "after", Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.IsThreadPoolThread }); } 

La salida:

 {donde = antes, ManagedThreadId = 10, IsThreadPoolThread = False}
 {where = ContinueWith, ManagedThreadId = 10, IsThreadPoolThread = False}
 {where = after, ManagedThreadId = 11, IsThreadPoolThread = True}

¿Por qué ConfigureAwait impulsa proactivamente la continuación de await a un hilo de grupo aquí?

Los documentos de MSDN dicen:

continueOnCapturedContext … true para intentar ordenar la continuación al contexto original capturado; de lo contrario, falso.

Entiendo que hay WinFormsSynchronizationContext instalado en el hilo actual. Aún así, no hay ningún bash de organizar la formación , el punto de ejecución ya está allí.

Por lo tanto, es más como “nunca continuar en el contexto original capturado”

Como se esperaba, no hay un cambio de subproceso si el punto de ejecución ya está en un subproceso de grupo sin un contexto de sincronización:

 await Task.Delay(100).ContinueWith(t => { // on a pool thread Debug.WriteLine(new { where = "ContinueWith", Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.IsThreadPoolThread }); }, TaskContinuationOptions.ExecuteSynchronously).ConfigureAwait(false); 
 {donde = antes, ManagedThreadId = 10, IsThreadPoolThread = False}
 {where = ContinueWith, ManagedThreadId = 6, IsThreadPoolThread = True}
 {where = after, ManagedThreadId = 6, IsThreadPoolThread = True}

Estoy a punto de ver la implementación de ConfiguredTaskAwaitable para las respuestas.

Actualizado , una prueba más para ver si hay alguna sincronización. el contexto no es lo suficientemente bueno para la continuación (en lugar del original). Este es de hecho el caso:

 class DumbSyncContext: SynchronizationContext { } // ... Debug.WriteLine(new { where = "before", Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.IsThreadPoolThread }); var tcs = new TaskCompletionSource(); var thread = new Thread(() => { Debug.WriteLine(new { where = "new Thread", Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.IsThreadPoolThread}); SynchronizationContext.SetSynchronizationContext(new DumbSyncContext()); tcs.SetResult(true); Thread.Sleep(1000); }); thread.Start(); await tcs.Task.ContinueWith(t => { Debug.WriteLine(new { where = "ContinueWith", Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.IsThreadPoolThread}); }, TaskContinuationOptions.ExecuteSynchronously).ConfigureAwait(false); Debug.WriteLine(new { where = "after", Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.IsThreadPoolThread }); 
 {donde = antes, ManagedThreadId = 9, IsThreadPoolThread = False}
 {where = new Thread, ManagedThreadId = 10, IsThreadPoolThread = False}
 {where = ContinueWith, ManagedThreadId = 10, IsThreadPoolThread = False}
 {where = after, ManagedThreadId = 6, IsThreadPoolThread = True}

¿Por qué ConfigureAwait impulsa proactivamente la continuación de espera a un hilo de grupo aquí?

No “lo empuja hacia un hilo de grupo de subprocesos” tanto como dice “no me fuerce a volver al SynchronizationContext anterior”.

Si no captura el contexto existente, entonces la continuación que maneja el código después de esa await solo se ejecutará en un hilo del grupo de subprocesos en su lugar, ya que no hay contexto en el que se pueda establecer una nueva asignación.

Ahora, esto es sutilmente diferente de “enviar a un grupo de subprocesos”, ya que no hay una garantía de que se ejecutará en un grupo de subprocesos cuando ConfigureAwait(false) . Si llamas:

 await FooAsync().ConfigureAwait(false); 

Es posible que FooAsync() se ejecute sincrónicamente, en cuyo caso, nunca abandonará el contexto actual. En ese caso, ConfigureAwait(false) no tiene ningún efecto real, ya que la máquina de estado creada por la función de await se cortocircuitará y solo se ejecutará directamente.

Si quieres ver esto en acción, crea un método asíncrono como ese:

 static Task FooAsync(bool runSync) { if (!runSync) await Task.Delay(100); } 

Si llamas esto como:

 await FooAsync(true).ConfigureAwait(false); 

Verá que se queda en el hilo principal (dado que era el contexto actual antes de esperar), ya que no hay un código asíncrono real ejecutándose en la ruta del código. La misma llamada con FooAsync(false).ConfigureAwait(false); Sin embargo, hará que salte al hilo del grupo de subprocesos después de la ejecución.

Aquí está la explicación de este comportamiento basado en la búsqueda del origen de referencia .NET .

Si se usa ConfigureAwait(true) , la continuación se realiza a través de TaskSchedulerAwaitTaskContinuation que usa SynchronizationContextTaskScheduler , todo está claro en este caso.

Si se usa ConfigureAwait(false) (o si no hay contexto de sincronización para capturar), se realiza a través de AwaitTaskContinuation , que trata de AwaitTaskContinuation la tarea de continuación primero, luego usa ThreadPool para ThreadPool en cola si no es posible ThreadPool .

La alineación está determinada por IsValidLocationForInlining , que nunca resume la tarea en un hilo con un contexto de sincronización personalizado. Sin embargo, lo mejor es alinearlo en el hilo del grupo actual. Eso explica por qué estamos empujados en un subproceso de agrupación en el primer caso y permanecemos en el mismo subproceso de agrupación en el segundo caso (con Task.Delay(100) ).

Creo que es más fácil pensar en esto de una manera ligeramente diferente.

Digamos que tienes:

 await task.ConfigureAwait(false); 

En primer lugar, si la task ya está completa, como señaló Reed, ConfigureAwait se ignora y la ejecución continúa (sincrónicamente, en el mismo hilo).

De lo contrario, await pausa el método. En ese caso, cuando se reanude y vea que ConfigureAwait es false , existe una lógica especial para verificar si el código tiene un SynchronizationContext y reanudar en un grupo de subprocesos si ese es el caso. Esto es un comportamiento no documentado pero no inapropiado. Como no está documentado, le recomiendo que no dependa del comportamiento; si desea ejecutar algo en el grupo de subprocesos, use Task.Run . ConfigureAwait(false) literalmente significa “No me importa en qué contexto se reanude este método”.

Tenga en cuenta que ConfigureAwait(true) (valor predeterminado) continuará el método en el SynchronizationContext o TaskScheduler actual. Mientras ConfigureAwait(false) continuará el método en cualquier hilo a excepción de uno con un SynchronizationContext . No son exactamente lo opuesto el uno del otro.