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 qu' TaskScheduler.Current
est la valeur par défaut du planificateur de tâches, pas d' TaskScheduler.Default
. Ce n'est absolument pas évidente au premier coup d'œil dans la documentation, ni des é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.
Supposons que j'écris 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() {
var handler = SomeOperationCompleted;
if (handler != null)
handler(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()
, 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();
}
private void DoSomethingElse() {
...
Task.Factory.StartNew(() => Thread.Sleep(5000)/*simulate a long operation*/);
...
}
Ici, la personne qui écrit l'appel de la bibliothèque a choisi de démarrer un nouveau Task
lorsque l'opération est terminée. Rien d'inhabituel. Il ou elle suit des exemples trouvés partout sur le web et l'utiliser Task.Factory.StartNew
, sans préciser le TaskScheduler
(et il n'est pas facile de surcharge de le spécifier lors de la deuxième paramètre). L' 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
réutilisent 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 avoir enveloppé 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
comme valeur par défaut:
- Il n'est pas évident à tous, en
Default
n'est pas la valeur par défaut! Et de la documentation en manque sérieusement. - La véritable tâche du planificateur utilisée 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.