Pourquoi est-TaskScheduler.Actuel par défaut TaskScheduler?
La Task Parallel Library est excellent et je l'ai utilisé beaucoup dans les derniers mois. Cependant, il y a vraiment quelque chose qui me tracasse: le fait que TaskScheduler.Courant
est la valeur par défaut du planificateur de tâches, pas TaskScheduler.Default
. Ce n'est absolument pas évidente au premier coup d'œil dans la documentation, ni échantillons.
Current
peut conduire à des bogues subtils depuis son comportement change selon que vous êtes à l'intérieur d'une autre tâche. Qui ne peut pas être facilement déterminée.
Suppose que je suis d'écrire une bibliothèque de méthodes asynchrones, à l'aide de la norme async modèle basé sur les événements de signal d'achèvement sur l'origine de la synchronisation contexte, exactement de la même façon XxxAsync méthodes ne dans le .NET Framework (par exemple DownloadFileAsync
). Je décide d'utiliser la Task Parallel Library pour la mise en œuvre parce que c'est vraiment facile à mettre en œuvre ce problème avec le code suivant:
public class MyLibrary
{
public event EventHandler SomeOperationCompleted;
private void OnSomeOperationCompleted()
{
SomeOperationCompleted?.Invoke(this, EventArgs.Empty);
}
public void DoSomeOperationAsync()
{
Task.Factory.StartNew(() =>
{
Thread.Sleep(1000); //simulate a long operation
}, CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Default)
.ContinueWith(t =>
{
OnSomeOperationCompleted(); //trigger the event
}, TaskScheduler.FromCurrentSynchronizationContext());
}
}
Jusqu'à présent, tout fonctionne bien. Maintenant, nous allons faire un appel à cette bibliothèque sur un bouton de la souris dans un WPF ou WinForms application:
private void Button_OnClick(object sender, EventArgs args)
{
var myLibrary = new MyLibrary();
myLibrary.SomeOperationCompleted += (s, e) => DoSomethingElse();
myLibrary.DoSomeOperationAsync(); //call that triggers the event asynchronously
}
private void DoSomethingElse() //the event handler
{
//...
Task.Factory.StartNew(() => Thread.Sleep(5000)); //simulate a long operation
//...
}
Ici, la personne qui écrit l'appel de la bibliothèque a choisi de lancer une nouvelle Task
lorsque l'opération est terminée. Rien d'inhabituel. Il ou elle suit des exemples trouvés partout sur le web et utilisez simplement Task.Factory.StartNew
sans préciser la TaskScheduler
(et il n'est pas facile de surcharge de le spécifier lors de la deuxième paramètre). Le DoSomethingElse
méthode fonctionne très bien lorsqu'il est appelé à lui seul, mais dès lors il est invoqué par l'événement, l'INTERFACE utilisateur se bloque depuis TaskFactory.Current
va réutiliser le contexte de synchronisation planificateur de tâches à partir de ma bibliothèque continuation.
Trouver cela pourrait prendre un certain temps, surtout si la deuxième tâche appel est enterré dans certains complexes de la pile des appels. Bien sûr, la solution est simple une fois que vous savez comment tout cela fonctionne: toujours spécifier TaskScheduler.Default
pour toute opération que vous craignez d'être en cours d'exécution sur le pool de threads. Cependant, peut-être la deuxième tâche est démarrée par une autre bibliothèque externe, ne pas connaître ce problème et naïvement à l'aide de StartNew
en l'absence d'un planificateur. Je m'attends à ce cas pour être tout à fait commun.
Après enveloppant ma tête autour de lui, je ne peux pas comprendre le choix de l'équipe de rédaction de la TPL à utiliser TaskScheduler.Current
au lieu de TaskScheduler.Default
par défaut:
- Ce n'est pas du tout évident,
Default
n'est pas la valeur par défaut! Et de la documentation manque sérieusement. - Le réel planificateur de tâches utilisés par
Current
dépend de la pile d'appel! Il est difficile de maintenir les invariants de ce comportement. - C'est gênant pour spécifier le planificateur de tâches avec
StartNew
puisque vous devez spécifier la tâche des options de création et d'annulation jeton premier, conduisant à la longue, moins lisible lignes. Cela peut être atténué par l'écriture d'une méthode d'extension ou de création d'unTaskFactory
qui utiliseDefault
. - La capture de la pile d'appel a en plus des coûts de l'exécution.
- Quand j'ai vraiment envie d'une tâche dépend d'un autre parent de l'exécution de la tâche, je préfère préciser explicitement à la facilité de lecture du code plutôt que de compter sur la pile d'appel à la magie.
Je sais que cette question peut sembler tout à fait subjectif, mais je ne peux pas trouver un bon objectif argument pour expliquer pourquoi ce comportement est comme ça. Je suis sûr que je suis absent quelque chose ici: c'est pourquoi je me tourne vers vous et vous.
- J'ai du mal à suivre votre exemple, exactement, mais ce n'est pas ici, la faute à la consommation de code (
DoSomethingElse
) en supposant que ça va être appelée dans le contexte de l'INTERFACE utilisateur? (Si c'est le point que vous essayez de faire - que c'est de la création des tâches pas dans l'INTERFACE utilisateur, contexte) - Il le contraire:
DoSomethingElse
pouvez exécuter dans n'importe quel contexte ici, mais dans ce cas précis, la tâche il crée s'exécuter dans le contexte d'une tâche parente, lui-même en cours d'exécution sur le thread de l'INTERFACE utilisateur, sans le savoir. Il n'y a pas de problème si leDefault
planificateur de tâches a été utilisé. Je n'ai pas de problème avec le préciser, mais je n'ai pas de contrôle sur chaque tiers de la bibliothèque, pas toujours conscients de ce fait. Ce que je ne comprends pas, c'est vraiment pourquoiCurrent
la valeur par défaut avec tous ceux qui sont potentiellement dangereux à l'évolution des contextes. Cette question est probablement trop argumentatif bien. - Dans .NET 4.5, il y a maintenant la Tâche.Exécuter, où TaskScheduler.Par défaut la valeur par défaut est TaskScheduler: blogs.msdn.com/b/pfxteam/archive/2011/10/24/10229468.aspx
- Avez-vous envisagé de faire référence explicitement à la thread de l'INTERFACE utilisateur, plutôt que de le faire avec le planificateur? Il semble comme une recette pour un désastre pour moi. Je suis d'accord avec vous, c'est assez dépourvues de logique sur le TPL de l'équipe de côté.
- Un autre article sur le blog: blog.stephencleary.com/2013/08/startnew-is-dangerous.html
Vous devez vous connecter pour publier un commentaire.
Je pense que le comportement actuel de sens. Si je créer mon propre planificateur de tâches, et de commencer une tâche qui commence d'autres tâches, je voudrez probablement toutes les tâches à utiliser le planificateur j'ai créé.
Je suis d'accord que c'est bizarre que parfois, à partir d'une tâche à partir du thread d'INTERFACE utilisateur utilise la valeur par défaut du planificateur et parfois pas. Mais je ne sais pas comment pourrais-je faire mieux si j'étais le concevoir.
Concernant vos problèmes spécifiques:
new Task(lambda).Start(scheduler)
. Ceci a l'inconvénient que vous devez spécifier le type de l'argument si la tâche renvoie à quelque chose.TaskFactory.Create
peut déduire le type pour vous.Dispatcher.Invoke()
au lieu d'utiliserTaskScheduler.FromCurrentSynchronizationContext()
.[MODIFIER]
La suite seulement résout le problème avec le planificateur utilisé par
Task.Factory.StartNew
.Cependant,
Task.ContinueWith
a une codé en durTaskScheduler.Current
.[/EDIT]
Tout d'abord, il ya une solution facile disponible - voir en bas de ce post.
La raison derrière ce problème est simple: Il n'est pas seulement un défaut planificateur de tâches (
TaskScheduler.Default
), mais aussi un défaut du planificateur de tâches pour unTaskFactory
(TaskFactory.Scheduler
).Ce défaut planificateur peut être spécifié dans le constructeur de la
TaskFactory
quand il est créé.Cependant, la
TaskFactory
derrièreTask.Factory
est créé comme suit:Comme vous pouvez le voir, pas de
TaskScheduler
est spécifié;null
est utilisé pour le constructeur par défaut - mieux seraitTaskScheduler.Default
(la documentation indique que le "Courant" est utilisé qui a les mêmes conséquences).Ce nouveau conduit à la mise en œuvre de
TaskFactory.DefaultScheduler
(un membre privé):Ici, vous devriez voir être en mesure de reconnaître la raison de ce comportement: la Tâche.L'usine n'a pas de défaut planificateur de tâches, le courant sera utilisé.
Alors, pourquoi ne pas courir dans
NullReferenceExceptions
puis, lorsqu'aucune Tâche n'est en cours d'exécution (c'est à dire nous n'avons pas de courant TaskScheduler)?La raison en est simple:
TaskScheduler.Current
par défautTaskScheduler.Default
.Je dirais que c'est une très malheureux de mise en œuvre.
Cependant, il est facile de fixer disponible: il suffit de définir la valeur par défaut
TaskScheduler
deTask.Factory
àTaskScheduler.Default
J'espère que je pourrais aider avec ma réponse même si elle est très en retard 🙂
TaskScheduler
est spécifié.Au lieu de
Task.Factory.StartNew()
envisager d'utiliser:
Task.Run()
Ce sera toujours exécuté sur un thread du pool. J'ai juste eu le même problème décrit dans la question et je pense que c'est une bonne façon de gérer cela.
Voir cette entrée de blog:
http://blogs.msdn.com/b/pfxteam/archive/2011/10/24/10229468.aspx
TaskCreationOptions.LongRunning
? Je crois que tous lesTask.Factory.StartNew()
ounew Task()
lieux peuvent être modifiés qu'avec l'Task.Run()
Default
est la valeur par défaut, mais il n'est pas toujours leCurrent
.Comme d'autres l'ont déjà répondu, si vous voulez exécuter une tâche sur le pool de threads, vous devez définir explicitement le
Current
planificateur en passant par leDefault
planificateur dans leTaskFactory
ou laStartNew
méthode.Étant donné que votre question a impliqué une bibliothèque bien, je pense que la réponse est que vous ne devriez pas faire quelque chose qui va changer le
Current
planificateur qui est vu par le code de l'extérieur de votre bibliothèque. Cela signifie que vous ne devez pas utiliserTaskScheduler.FromCurrentSynchronizationContext()
lorsque vous soulevez leSomeOperationCompleted
événement. Au lieu de cela, faire quelque chose comme ceci:Je ne pense pas que vous avez besoin pour démarrer explicitement votre tâche sur le
Default
planificateur de - l'appelant de déterminer laCurrent
planificateur s'ils le souhaitent.Je viens de passer des heures à essayer de déboguer un étrange problème où ma tâche a été planifiée sur le thread de l'INTERFACE utilisateur, même si je n'ai pas le préciser à. Il s'est avéré que le problème était exactement ce que votre exemple de code démontré: Une tâche de poursuite est prévue sur le thread d'INTERFACE utilisateur, et quelque part dans cette poursuite, une nouvelle tâche a commencé, qui a ensuite obtenu prévue sur le thread de l'INTERFACE utilisateur, car en cours d'exécution de la tâche a un
TaskScheduler
ensemble.Heureusement, c'est tout le code que je possède, afin que je puisse corriger en faisant en sorte que mon code spécifier
TaskScheduler.Default
lors du démarrage de nouvelles tâches, mais si vous n'êtes pas aussi chanceux, ma suggestion serait d'utiliserDispatcher.BeginInvoke
au lieu d'utiliser l'INTERFACE utilisateur du planificateur.Donc, au lieu de:
Essayer:
C'est un peu moins lisible si.