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.Task
WinFormsSynchronizationContext
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 classeTestSyncContext
juste 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.
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.Current
new 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 parawait tcs.Task
.- à la fin du
StartNew
tâche, il exécute de façon synchrone la suite définie parawait 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 aTaskScheduler.Current
SynchronizationContextTaskScheduler
dontm_synchronizationContext
est l'instance créée parnew SynchronizationContext()
; cependant, du threadSynchronizationContext.Current
null
.SetResult
tente d'exécuter leawait tcs.Task
continuation synchrone sur l'ordonnanceur de tâches actuel; cependant, il ne peut pas parce queSynchronizationContextTaskScheduler
voit ce thread 10 a unSynchronizationContext.Current
null
alors qu'il faut unnew 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 leawait 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.Current
new SynchronizationContext()
, parce que le traitement de ce scénario est incohérent.
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