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'un TaskFactory qui utilise Default.
  • 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 le Default 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 pourquoi Current 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