62 votes

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 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'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.

18voto

svick Points 81772

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 à la concevoir.

Concernant vos problèmes spécifiques:

  • Je pense que la meilleure façon de démarrer une nouvelle tâche sur un certain planificateur 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 pouvons en déduire que le type pour vous.
  • Vous pouvez utiliser Dispatcher.Invoke() au lieu d'utiliser TaskScheduler.FromCurrentSynchronizationContext().

7voto

winSharp93 Points 7124

[MODIFIER] La suite seulement résout le problème avec le planificateur utilisée par Task.Factory.StartNew.
Toutefois, Task.ContinueWith a une codé en dur TaskScheduler.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 un TaskFactory (TaskFactory.Scheduler). Ce défaut planificateur peut être spécifié dans le constructeur de l' TaskFactory quand il est créé.

Cependant, l' TaskFactory derrière Task.Factory est créé comme suit:

s_factory = new TaskFactory();

Comme vous pouvez le voir, pas de TaskFactory est spécifié; null est utilisé pour le constructeur par défaut - mieux serait TaskScheduler.Default (la documentation indique que le "Courant" est utilisé qui a les mêmes conséquences).
Ce nouveau conduit à la mise en œuvre de l' TaskFactory.DefaultScheduler (un membre privé):

private TaskScheduler DefaultScheduler 
{ 
   get
   { 
      if (m_defaultScheduler == null) return TaskScheduler.Current;
      else return m_defaultScheduler;
   }
}

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 exécuter en 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:

public static TaskScheduler Current
{
    get
    {
        Task internalCurrent = Task.InternalCurrent;
        if (internalCurrent != null)
        {
            return internalCurrent.ExecutingTaskScheduler;
        }
        return Default;
    }
}

TaskScheduler.Current par défaut est TaskScheduler.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 de Task.Factory de TaskScheduler.Default

TaskFactory factory = Task.Factory;
factory.GetType().InvokeMember("m_defaultScheduler", BindingFlags.SetField | BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.DeclaredOnly, null, factory, new object[] { TaskScheduler.Default });

J'espère que je pourrais aider avec ma réponse même si elle est très en retard :-)

5voto

testalino Points 2554

Au lieu de Task.Factory.StartNew()

pensez à 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

3voto

Rory MacLeod Points 4574

Ce n'est pas du tout évident, par Défaut n'est pas la valeur par défaut! Et de la documentation en manque sérieusement.

Default est la valeur par défaut, mais il n'est pas toujours l' Current.

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 l' Current planificateur en passant l' Default planificateur dans l' TaskFactory ou StartNew 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 l' Current planificateur qui est vu par le code de l'extérieur de votre bibliothèque. Cela signifie que vous ne devez pas utiliser TaskScheduler.FromCurrentSynchronizationContext() lorsque vous soulevez l' SomeOperationCompleted événement. Au lieu de cela, faire quelque chose comme ceci:

public void DoSomeOperationAsync() {
    var context = SynchronizationContext.Current;
    Task.Factory
        .StartNew(() => Thread.Sleep(1000) /* simulate a long operation */)
        .ContinueWith(t => {
            context.Post(_ => OnSomeOperationCompleted(), null);
        });
}

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 l' Current planificateur s'ils le souhaitent.

0voto

René Points 4134

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 spécifier. 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'utiliser Dispatcher.BeginInvoke au lieu d'utiliser l'INTERFACE utilisateur du planificateur.

Ainsi, au lieu de:

var uiScheduler = TaskScheduler.FromCurrentSynchronizationContext();
var task = Task.Factory.StartNew(() => Thread.Sleep(5000));
task.ContinueWith((t) => UpdateUI(), uiScheduler);

Essayez:

var uiDispatcher = Dispatcher.CurrentDispatcher;
var task = Task.Factory.StartNew(() => Thread.Sleep(5000));
task.ContinueWith((t) => uiDispatcher.BeginInvoke(new Action(() => UpdateUI())));

C'est un peu moins lisible.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X