program net job c# .net multithreading task-parallel-library async-await

c# - net - Default SynchronizationContext vs Default TaskScheduler



set task scheduler c# (2)

Cuando comienza a profundizar en los detalles de implementación, es importante diferenciar entre el comportamiento documentado / confiable y el comportamiento no documentado. Además, realmente no se considera adecuado tener SynchronizationContext.Current configurado en el new SynchronizationContext() ; algunos tipos en .NET tratan null como el planificador predeterminado, y otros tipos tratan null o new SynchronizationContext() como el planificador predeterminado.

Cuando TaskAwaiter una Task incompleta, TaskAwaiter captura de forma predeterminada el SynchronizationContext actual, a menos que sea null (o GetType devuelve typeof(SynchronizationContext) ), en cuyo caso TaskAwaiter captura el TaskScheduler actual. Este comportamiento está principalmente documentado (la cláusula GetType no es AFAIK). Sin embargo, tenga en cuenta que esto describe el comportamiento de TaskAwaiter , no TaskScheduler.Default o TaskFactory.StartNew .

Después de capturar el contexto (si hay alguno), await horarios una continuación. Esta continuación está programada con ExecuteSynchronously , como se describe en mi blog (este comportamiento no está documentado). Sin embargo, tenga en cuenta que ExecuteSynchronously no siempre se ejecuta de forma síncrona ; en particular, si una continuación tiene un planificador de tareas, solo solicitará ejecutar de forma síncrona en el hilo actual, y el planificador de tareas tiene la opción de rechazar ejecutarlo de forma síncrona (también indocumentado).

Finalmente, tenga en cuenta que se le puede solicitar a TaskScheduler que ejecute una tarea de forma síncrona, pero un SynchronizationContext no puede. Entonces, si el await captura un SynchronizationContext personalizado, entonces siempre debe ejecutar la continuación de forma asincrónica.

Por lo tanto, en su prueba original n. ° 1:

  • StartNew inicia una nueva tarea con el programador de tareas predeterminado (en el hilo 10).
  • SetResult ejecuta de forma síncrona el conjunto de continuación mediante la await tcs.Task .
  • Al final de la tarea StartNew , ejecuta sincrónicamente el conjunto de continuación mediante la await task .

En su prueba original n. ° 2:

  • StartNew inicia una nueva tarea con un contenedor de programador de tareas para un contexto de sincronización construido por defecto (en el hilo 10). Tenga en cuenta que la tarea en el hilo 10 tiene TaskScheduler.Current configurado en SynchronizationContextTaskScheduler cuyo m_synchronizationContext es la instancia creada por el new SynchronizationContext() ; sin embargo, SynchronizationContext.Current ese subproceso es null .
  • SetResult intenta ejecutar la continuación de await tcs.Task sincrónicamente en el planificador de tareas actual; sin embargo, no puede porque SynchronizationContextTaskScheduler ve que el hilo 10 tiene un SynchronizationContext.Current of null mientras requiere un new SynchronizationContext() . Por lo tanto, programa la continuación de forma asincrónica (en el hilo 11).
  • Una situación similar ocurre al final de la tarea de StartNew ; en este caso, creo que es una coincidencia que la await task continúe en el mismo hilo.

En conclusión, debo enfatizar que depender de los detalles de la implementación indocumentada no es prudente. Si desea que su método async continúe en un hilo del grupo de subprocesos, envuélvalo en una Task.Run . Task.Run . Eso hará que la intención de su código sea mucho más clara, y también hará que su código sea más resistente a futuras actualizaciones del marco. Además, no configure SynchronizationContext.Current en el new SynchronizationContext() , ya que el manejo de ese escenario es incoherente.

Esto va a ser un poco largo, así que por favor tengan paciencia conmigo.

Estaba pensando que el comportamiento del programador de tareas predeterminado ( ThreadPoolTaskScheduler ) es muy similar al del SynchronizationContext " ThreadPool " predeterminado (este último se puede referenciar implícitamente mediante TaskScheduler.FromCurrentSynchronizationContext() o explícitamente a través de TaskScheduler.FromCurrentSynchronizationContext() ). Ambos programan tareas que se ejecutarán en un hilo aleatorio de ThreadPool . De hecho, SynchronizationContext.Post simplemente llama a ThreadPool.QueueUserWorkItem .

Sin embargo, hay una diferencia sutil pero importante en cómo funciona TaskCompletionSource.SetResult , cuando se utiliza desde una tarea en cola en el SynchronizationContext predeterminado. Aquí hay una aplicación de consola simple que lo ilustra:

using System; using System.Threading; using System.Threading.Tasks; namespace ConsoleTcs { class Program { static async Task TcsTest(TaskScheduler taskScheduler) { var tcs = new TaskCompletionSource<bool>(); var task = Task.Factory.StartNew(() => { Thread.Sleep(1000); Console.WriteLine("before tcs.SetResult, thread: " + Thread.CurrentThread.ManagedThreadId); tcs.SetResult(true); Console.WriteLine("after tcs.SetResult, thread: " + Thread.CurrentThread.ManagedThreadId); Thread.Sleep(2000); }, CancellationToken.None, TaskCreationOptions.None, taskScheduler); Console.WriteLine("before await tcs.Task, thread: " + Thread.CurrentThread.ManagedThreadId); await tcs.Task.ConfigureAwait(true); Console.WriteLine("after await tcs.Task, thread: " + Thread.CurrentThread.ManagedThreadId); await task.ConfigureAwait(true); Console.WriteLine("after await task, thread: " + Thread.CurrentThread.ManagedThreadId); } // Main static void Main(string[] args) { // SynchronizationContext.Current is null // install default SynchronizationContext on the thread SynchronizationContext.SetSynchronizationContext(new SynchronizationContext()); // use TaskScheduler.Default for Task.Factory.StartNew Console.WriteLine("Test #1, thread: " + Thread.CurrentThread.ManagedThreadId); TcsTest(TaskScheduler.Default).Wait(); // use TaskScheduler.FromCurrentSynchronizationContext() for Task.Factory.StartNew Console.WriteLine("/nTest #2, thread: " + Thread.CurrentThread.ManagedThreadId); TcsTest(TaskScheduler.FromCurrentSynchronizationContext()).Wait(); Console.WriteLine("/nPress enter to exit, thread: " + Thread.CurrentThread.ManagedThreadId); Console.ReadLine(); } } }

La salida:

Test #1, thread: 9 before await tcs.Task, thread: 9 before tcs.SetResult, thread: 10 after await tcs.Task, thread: 10 after tcs.SetResult, thread: 10 after await task, thread: 10 Test #2, thread: 9 before await tcs.Task, thread: 9 before tcs.SetResult, thread: 10 after tcs.SetResult, thread: 10 after await tcs.Task, thread: 11 after await task, thread: 11 Press enter to exit, thread: 9

Esta es una aplicación de consola, su hilo Main no tiene ningún contexto de sincronización de forma predeterminada, por lo que instalo explícitamente el predeterminado al principio, antes de ejecutar las pruebas: SynchronizationContext.SetSynchronizationContext(new SynchronizationContext()) .

Inicialmente, pensé que comprendí completamente el flujo de trabajo de ejecución durante la prueba n. ° 1 (donde la tarea está programada con TaskScheduler.Default ). There tcs.SetResult invoca de forma sincrónica la primera parte de continuación ( await tcs.Task ), luego el punto de ejecución vuelve a tcs.SetResult y continúa sincrónicamente para siempre, incluida la segunda await task . Eso tuvo sentido para mí, hasta que me di cuenta de lo siguiente . Como ahora tenemos el contexto de sincronización predeterminado instalado en el hilo que await tcs.Task , debe capturarse y la continuación debe ocurrir de forma asincrónica (es decir, en un hilo de grupo diferente puesto en cola por SynchronizationContext.Post ). Por analogía, si ejecuté la prueba n. ° 1 desde una aplicación de WinForms, se habría continuado de forma asíncrona después de await tcs.Task , en WinFormsSynchronizationContext en una futura iteración del ciclo de mensajes.

Pero eso no es lo que sucede dentro de la prueba n. ° 1. Por curiosidad, cambié ConfigureAwait(true) a ConfigureAwait(false) y eso no tuvo ningún efecto en la salida. Estoy buscando una explicación de esto.

Ahora, durante la prueba n. ° 2 (la tarea está programada con TaskScheduler.FromCurrentSynchronizationContext() ), de hecho hay un cambio de TaskScheduler.FromCurrentSynchronizationContext() más, en comparación con el n. ° 1. Como se puede ver en el resultado, la continuación de await tcs.Task activada por tcs.SetResult se produce de forma asincrónica en otro subproceso del grupo. Intenté con ConfigureAwait(false) también, eso tampoco cambió nada. También traté de instalar SynchronizationContext inmediatamente antes de comenzar la prueba n. ° 2 , en lugar de hacerlo al principio. Eso dio como resultado exactamente el mismo resultado, tampoco.

De hecho, me gusta más el comportamiento de la prueba n. ° 2, porque deja menos espacio para los efectos secundarios (y, potencialmente, interbloqueos) que pueden ser causados ​​por la continuación síncrona activada por tcs.SetResult , aunque a un precio de un interruptor de hilo adicional. Sin embargo, no entiendo completamente por qué dicho cambio de hilo tiene lugar independientemente de ConfigureAwait(false) .

Estoy familiarizado con los siguientes excelentes recursos sobre el tema, pero todavía estoy buscando una buena explicación de los comportamientos vistos en las pruebas n. ° 1 y n. ° 2. ¿Alguien puede dar más detalles sobre esto?

La naturaleza de TaskCompletionSource
Programación en paralelo: Programadores de tareas y contexto de sincronización
Programación paralela: TaskScheduler.FromCurrentSynchronizationContext
Se trata del SynchronizationContext

[ACTUALIZACIÓN] Lo que quiero decir es que el objeto de contexto de sincronización predeterminado se ha instalado explícitamente en el hilo principal, antes de que el hilo await tcs.Task al primer await tcs.Task En la prueba n.º 1. IMO, el hecho de que no es un contexto de sincronización GUI no significa que no deba capturarse para continuar después de await . Es por eso que espero que la continuación después de tcs.SetResult tenga lugar en un hilo diferente de ThreadPool (en cola allí por SynchronizationContext.Post ), mientras que el hilo principal aún puede ser bloqueado por TcsTest(...).Wait() . Este es un escenario muy similar al descrito aquí .

Así que seguí adelante e implementé una clase de contexto de sincronización tonta TestSyncContext , que es solo una envoltura alrededor de SynchronizationContext . Ahora está instalado en lugar del propio SynchronizationContext :

using System; using System.Threading; using System.Threading.Tasks; namespace ConsoleTcs { public class TestSyncContext : SynchronizationContext { public override void Post(SendOrPostCallback d, object state) { Console.WriteLine("TestSyncContext.Post, thread: " + Thread.CurrentThread.ManagedThreadId); base.Post(d, state); } public override void Send(SendOrPostCallback d, object state) { Console.WriteLine("TestSyncContext.Send, thread: " + Thread.CurrentThread.ManagedThreadId); base.Send(d, state); } }; class Program { static async Task TcsTest(TaskScheduler taskScheduler) { var tcs = new TaskCompletionSource<bool>(); var task = Task.Factory.StartNew(() => { Thread.Sleep(1000); Console.WriteLine("before tcs.SetResult, thread: " + Thread.CurrentThread.ManagedThreadId); tcs.SetResult(true); Console.WriteLine("after tcs.SetResult, thread: " + Thread.CurrentThread.ManagedThreadId); Thread.Sleep(2000); }, CancellationToken.None, TaskCreationOptions.None, taskScheduler); Console.WriteLine("before await tcs.Task, thread: " + Thread.CurrentThread.ManagedThreadId); await tcs.Task.ConfigureAwait(true); Console.WriteLine("after await tcs.Task, thread: " + Thread.CurrentThread.ManagedThreadId); await task.ConfigureAwait(true); Console.WriteLine("after await task, thread: " + Thread.CurrentThread.ManagedThreadId); } // Main static void Main(string[] args) { // SynchronizationContext.Current is null // install default SynchronizationContext on the thread SynchronizationContext.SetSynchronizationContext(new TestSyncContext()); // use TaskScheduler.Default for Task.Factory.StartNew Console.WriteLine("Test #1, thread: " + Thread.CurrentThread.ManagedThreadId); TcsTest(TaskScheduler.Default).Wait(); // use TaskScheduler.FromCurrentSynchronizationContext() for Task.Factory.StartNew Console.WriteLine("/nTest #2, thread: " + Thread.CurrentThread.ManagedThreadId); TcsTest(TaskScheduler.FromCurrentSynchronizationContext()).Wait(); Console.WriteLine("/nPress enter to exit, thread: " + Thread.CurrentThread.ManagedThreadId); Console.ReadLine(); } } }

¡Mágicamente, las cosas han cambiado de una mejor manera! Aquí está el nuevo resultado:

Test #1, thread: 10 before await tcs.Task, thread: 10 before tcs.SetResult, thread: 6 TestSyncContext.Post, thread: 6 after tcs.SetResult, thread: 6 after await tcs.Task, thread: 11 after await task, thread: 6 Test #2, thread: 10 TestSyncContext.Post, thread: 10 before await tcs.Task, thread: 10 before tcs.SetResult, thread: 11 TestSyncContext.Post, thread: 11 after tcs.SetResult, thread: 11 after await tcs.Task, thread: 12 after await task, thread: 12 Press enter to exit, thread: 10

Ahora la prueba # 1 ahora se comporta como se esperaba ( await tcs.Task se pone en cola asíncronamente a un subproceso de grupo). # 2 parece estar bien, también. Cambiemos ConfigureAwait(true) a ConfigureAwait(false) :

Test #1, thread: 9 before await tcs.Task, thread: 9 before tcs.SetResult, thread: 10 after await tcs.Task, thread: 10 after tcs.SetResult, thread: 10 after await task, thread: 10 Test #2, thread: 9 TestSyncContext.Post, thread: 9 before await tcs.Task, thread: 9 before tcs.SetResult, thread: 11 after tcs.SetResult, thread: 11 after await tcs.Task, thread: 10 after await task, thread: 10 Press enter to exit, thread: 9

La prueba n. ° 1 todavía se comporta correctamente como se esperaba: ConfigureAwait(false) hace que la await tcs.Task ignore el contexto de sincronización (la llamada TestSyncContext.Post se ha ido), por lo que ahora continúa de forma síncrona después de tcs.SetResult .

¿Por qué es esto diferente del caso cuando se usa SynchronizationContext defecto? Todavía tengo curiosidad por saber. Tal vez, el programador de tareas predeterminado (que es responsable de las continuidades de await ) comprueba la información del tipo de tiempo de ejecución del contexto de sincronización de la secuencia, y da un tratamiento especial a SynchronizationContext ?

Ahora, todavía no puedo explicar el comportamiento de la prueba # 2 para cuando ConfigureAwait(false) . Es una llamada menos TestSyncContext.Post , eso se entiende. Sin embargo, await tcs.Task todavía se continúa en un hilo diferente de tcs.SetResult (a diferencia del n. ° 1), eso no es lo que esperaría. Todavía estoy buscando una razón para esto.


SynchronizationContext simplemente llama a ThreadPool.QueueUserWorkItem en la publicación, lo que explica por qué siempre se ve un hilo diferente en la prueba n.º 2.

En la prueba n. ° 1 está utilizando un TaskScheduler más inteligente. await se supone que debe continuar en el mismo hilo (o "permanecer en el hilo actual" ). En una aplicación de consola no hay forma de "programar" regresar al hilo principal como lo hace en los marcos de interfaz de usuario basados ​​en cola de mensajes. Una await en una aplicación de consola tendría que bloquear el hilo principal hasta que el trabajo esté terminado (dejando el hilo principal sin nada que hacer) para continuar en ese mismo hilo. Si el planificador lo sabe , entonces también podría ejecutar el código de forma sincrónica en el mismo subproceso, ya que tendría el mismo resultado sin tener que crear otro subproceso y arriesgarse a un cambio de contexto.

Puede encontrar más información aquí: http://blogs.msdn.com/b/pfxteam/archive/2012/01/20/10259049.aspx

Actualización : en términos de ConfigureAwait . Las aplicaciones de consola no tienen forma de "ordenar" de vuelta al hilo principal, así que, presumiblemente, ConfigureAwait(false) no significa nada en una aplicación de consola.

Ver también: http://msdn.microsoft.com/en-us/magazine/jj991977.aspx