SynchronizationContext vs Default TaskScheduler

Cela va être un peu long, donc s'il vous plaît garder avec moi.

je pensais que le comportement par défaut du planificateur de tâches (ThreadPoolTaskScheduler) est très similaire à celle de la valeur par défaut "ThreadPool"SynchronizationContext (cette dernière peut être référencée implicitement via await ou explicitement via TaskScheduler.FromCurrentSynchronizationContext()). Ils planifient tous les deux des tâches à exécuter au hasard ThreadPool thread. En fait, SynchronizationContext.Post simplement des appels ThreadPool.QueueUserWorkItem.

cependant, il y a un subtil mais important différence dans la façon dont TaskCompletionSource.SetResult œuvres, lorsqu'il est utilisé à partir d'une tâche en file d'attente sur la valeur par défaut SynchronizationContext. Voici une simple application de console qui l'illustre:

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();
        }
    }
}

Le résultat:

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

C'est une application console, ses Main thread n'a pas de contexte de synchronisation par défaut, donc j'installe explicitement le contexte par défaut au début, avant d'exécuter des tests:SynchronizationContext.SetSynchronizationContext(new SynchronizationContext()).

au départ, je pensais avoir parfaitement compris le flux d'exécution pendant le test #1 (où la tâche est planifiée avec TaskScheduler.Default). Il y a tcs.SetResult invoque de façon synchrone la première partie de la suite (await tcs.Task), puis le point d'exécution retourne à tcs.SetResult et continue synchrone jamais après, y compris le deuxième await task. Cela a du sens pour moi, jusqu'à ce que je réalise ce qui suit. Comme nous avons maintenant le contexte de synchronisation par défaut installé sur le thread qui fait await tcs.Task, il devrait être saisi et la suite devrait se produire de façon asynchrone (c'est à dire, sur un autre pool de thread comme en file d'attente par SynchronizationContext.Post). Par analogie, si j'avais passé le test #1 à partir d'une application WinForms, il aurait été poursuivi de façon asynchrone après await tcs.TaskWinFormsSynchronizationContext sur une itération future de la boucle de message.

mais ce n'est pas ce qui se passe dans le test #1. Par curiosité, j'ai changé ConfigureAwait(true)ConfigureAwait(false) et aucun effet sur la sortie. Je suis à la recherche d'un l'explication de cette.

maintenant, pendant le test #2 (la tâche est prévue avec TaskScheduler.FromCurrentSynchronizationContext()) il y a en effet un interrupteur de plus, par rapport au #1. Comme on peut le voir à partir de la sortie, le await tcs.Task suite déclenchée par tcs.SetResult se produit de façon asynchrone, sur un autre fil de billard. J'ai essayé ConfigureAwait(false) trop, qui n'a rien changé non plus. J'ai aussi essayé d'installer SynchronizationContext immédiatement avant de commencer le test #2, plutôt qu'au début. Qui a abouti à exactement le même résultat, soit.

j'aime en fait le comportement du test #2 plus, parce qu'il laisse moins d'espace pour les effets secondaires (et, potentiellement, des blocages) qui peuvent être causés par la continuation synchrone déclenchée par tcs.SetResult, même si elle est offerte au prix d'un interrupteur à filetage supplémentaire. Cependant, je ne comprends pas tout pourquoi un tel changement de thread a lieu indépendamment de ConfigureAwait(false).

je connais les excellentes ressources suivantes sur le sujet, mais je suis toujours à la recherche d'une bonne explication des comportements vus dans l'essai #1 et #2. quelqu'un peut-il donner des détails à ce sujet?

La Nature de TaskCompletionSource

programmation parallèle: ordonnanceurs de tâches et contexte de synchronisation

Programmation Parallèle: TaskScheduler.FromCurrentSynchronizationContext

tout est dans le SynchronizationContext


[UPDATE] My point is, l'objet de contexte de synchronisation par défaut a été explicitement installé sur le thread principal, avant que le thread ne touche le premier await tcs.Task dans l'essai no 1. IMO, le fait que ce n'est pas un contexte de synchronisation GUI ne signifie pas qu'il ne devrait pas être capturé pour la suite après await. C'est pourquoi j'attends de la poursuite après tcs.SetResult pour prendre place sur un thread différent de l' ThreadPool (en attente ici par SynchronizationContext.Post), tandis que le thread principal peut encore être bloqué par TcsTest(...).Wait(). C'est très un scénario similaire à l' on décrit ici.

Donc, je suis allé de l'avant et mise en œuvre d'un muet contexte de synchronisation de la classeTestSyncContextjuste un wrapper autour de SynchronizationContext. Il est maintenant installé à la place du SynchronizationContext lui-même:

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();
        }
    }
}

par magie, les choses ont changé en une meilleure façon! Voici la nouvelle sortie:

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

maintenant le test #1 se comporte maintenant comme prévu (await tcs.Task est asynchrone à un fil de billard). #2 semble être OK, aussi. Nous allons changer ConfigureAwait(true)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

Test #1 continue de se comporter correctement comme prévu: ConfigureAwait(false)await tcs.Task ignorer le contexte de synchronisation (le TestSyncContext.Post l'appel est parti), donc maintenant il continue de façon synchrone après tcs.SetResult.

Pourquoi est-ce différent du cas où, par défaut,SynchronizationContext est-il utilisé? je suis toujours curieux de savoir. Peut-être, le planificateur de tâche par défaut (qui est responsable de await continuations) vérifie les informations de type runtime du contexte de synchronisation du thread, et donne un traitement spécial à <!--7?

maintenant, je ne peux toujours pas expliquer le comportement du test #2 pour quand ConfigureAwait(false). C'est un de moins TestSyncContext.Post appel, c'est entendu. Cependant, await tcs.Task se poursuit encore sur un autre fil de tcs.SetResult (contrairement à la première), ce n'est pas ce à quoi je m'attendais. Je suis toujours à la recherche d'une raison pour cela.

19
demandé sur noseratio 2014-01-06 06:59:35

2 réponses

lorsque vous commencez à plonger aussi profondément dans les détails de la mise en œuvre, il est important de faire la différence entre un comportement documenté/fiable et un comportement non documenté. Aussi, il n'est pas vraiment bon d'avoir SynchronizationContext.Currentnew SynchronizationContext(); certains types de. Net treat null comme le scheduler par défaut, et d'autres types traitent null ounew SynchronizationContext() comme planificateur par défaut.

quand vous await incomplète Task, le TaskAwaiter par défaut capture la SynchronizationContext - à moins que ce ne soit null (ou GetType retourne typeof(SynchronizationContext)), auquel cas le TaskAwaiter capture la TaskScheduler. Ce comportement est surtout documenté (le GetType l'article n'est pas autant que je sache). Cependant, veuillez noter que ceci décrit le comportement de TaskAwaiter, pas TaskScheduler.Default ou TaskFactory.StartNew.

après que le contexte (s'il y en a) est capturé, puis await planifie une suite. Cette poursuite est prévue en utilisant ExecuteSynchronously, comme décrit sur mon blog (ce le comportement est sans-papiers). Toutefois, notez que ExecuteSynchronously n'exécute pas toujours de façon synchrone; en particulier, si une suite a un planificateur de tâches, il ne demande pour exécuter de façon synchrone sur le thread courant, et le planificateur de tâches a l'option de refuser de l'exécuter de façon synchrone (également non documentée).

enfin, notez que a TaskScheduler peut être demandé pour exécuter une tâche de façon synchrone, mais un SynchronizationContext ne peut pas. Donc, si l' await capture une coutume SynchronizationContext, alors il doit toujours exécuter la suite de façon asynchrone.

donc, dans votre Test d'origine #1:

  • StartNew commence une nouvelle tâche avec le planificateur de tâche par défaut (sur thread 10).
  • SetResult exécute de façon synchrone le jeu de continuation par await tcs.Task.
  • à la fin du StartNew tâche, il exécute de façon synchrone la suite définie par await task.

Dans votre Test original #2:

  • StartNew commence une nouvelle tâche avec un wrapper de planificateur de tâche pour un contexte de synchronisation par défaut (sur thread 10). Notez que la tâche sur le fil 10 a TaskScheduler.CurrentSynchronizationContextTaskScheduler dont m_synchronizationContext est l'instance créée par new SynchronizationContext(); cependant, du thread SynchronizationContext.Currentnull.
  • SetResult tente d'exécuter le await tcs.Task continuation synchrone sur l'ordonnanceur de tâches actuel; cependant, il ne peut pas parce que SynchronizationContextTaskScheduler voit ce thread 10 a un SynchronizationContext.Currentnull alors qu'il faut un new SynchronizationContext(). Ainsi, il planifie la continuation de façon asynchrone (sur le fil 11).
  • une situation similaire se produit à la fin du StartNew tâche; dans ce cas, je crois que c'est une coïncidence que le await task continue sur le même thread.

en conclusion, je dois souligner qu'il n'est pas sage de dépendre de détails de mise en oeuvre non documentés. Si vous voulez avoir votre async méthode continuer sur un filez le fil de la piscine, puis enveloppez-le dans un Task.Run. Cela rendra l'intention de votre code beaucoup plus claire, et aussi rendre votre code plus résistant aux futures mises à jour du cadre. Aussi, ne pas set SynchronizationContext.Currentnew SynchronizationContext(), parce que le traitement de ce scénario est incohérent.

17
répondu Stephen Cleary 2014-01-06 18:29:35

SynchronizationContext toujours appelle tout simplement ThreadPool.QueueUserWorkItem sur le post--ce qui explique pourquoi vous voyez toujours un fil différent dans le test #2.

dans le test #1 vous utilisez un plus intelligent TaskScheduler. await est censé continuer sur le même thread (ou "rester sur le thread en cours"). Dans une application de Console, il n'y a aucun moyen de "programmer" le retour vers le thread principal comme il y en a dans les cadres UI basés sur les messages-queue. await dans une application console devrait bloquer le thread principal jusqu'à ce que le travail est fait (laissant le fil principal sans rien à faire) afin de continuer sur ce même fil. Si le planificateur le sait ceci, alors il pourrait aussi bien exécuter le code de manière synchrone sur le même thread qu'il aurait le même résultat sans avoir à créer un autre thread et risquer un commutateur de contexte.

vous trouverez plus d'informations ici: http://blogs.msdn.com/b/pfxteam/archive/2012/01/20/10259049.aspx

mise à Jour: En termes de ConfigureAwait. Les applications de Console n'ont aucun moyen de "marshal" retour au fil principal donc, probablement,ConfigureAwait(false) signifie rien dans une application de console.

Voir aussi: http://msdn.microsoft.com/en-us/magazine/jj991977.aspx

4
répondu Peter Ritchie 2014-01-06 10:43:41