web-dev-qa-db-fra.com

Est-ce que Task.Factory.StartNew () est garanti pour utiliser un autre thread que le thread appelant?

Je commence une nouvelle tâche à partir d'une fonction mais je ne voudrais pas qu'elle s'exécute sur le même thread. Je me moque du fil sur lequel il tourne tant qu'il est différent (les informations données dans cette question n'aident donc pas).

Suis-je assuré que le code ci-dessous quittera toujours TestLock avant de permettre à Task t de le saisir à nouveau? Si ce n’est pas le cas, quel est le modèle de conception recommandé pour empêcher la réintroduction?

object TestLock = new object();

public void Test(bool stop = false) {
    Task t;
    lock (this.TestLock) {
        if (stop) return;
        t = Task.Factory.StartNew(() => { this.Test(stop: true); });
    }
    t.Wait();
}

Edit: Sur la base de la réponse ci-dessous de Jon Skeet et Stephen Toub, un moyen simple d'empêcher de manière déterministe la réentrance serait de passer un CancellationToken, comme illustré dans cette méthode d'extension:

public static Task StartNewOnDifferentThread(this TaskFactory taskFactory, Action action) 
 {
    return taskFactory.StartNew(action: action, cancellationToken: new CancellationToken());
}
73
Erwin Mayer

J'ai envoyé un mail à Stephen Toub - un membre de l'équipe PFX - à propos de cette question. Il me revient très vite, avec beaucoup de détails - je vais donc copier et coller son texte ici. Je n'ai pas tout cité, car la lecture d'un grand nombre de textes cités finit par être moins confortable que la version noir sur blanc de Vanilla, mais en réalité, c'est Stephen - je ne connais pas beaucoup de choses :) ce wiki de la communauté de réponses afin de refléter le fait que toutes les qualités ci-dessous ne sont pas vraiment mon contenu:

Si vous appelez Wait() sur une tâche terminée, il n'y aura pas de blocage (une exception sera générée si la tâche est terminée avec un TaskStatus autre que RanToCompletion ou sinon retourne en tant que nop ). Si vous appelez Wait() pour une tâche en cours d'exécution, elle doit bloquer comme rien ne peut raisonnablement être fait (quand je dis bloquer, j'inclue à la fois les véritables attente et rotation sur le noyau, car il s'agit généralement d'un mélange de tous les deux). De même, si vous appelez Wait() sur une tâche ayant le statut Created ou WaitingForActivation, elle sera bloquée jusqu’à ce que la tâche soit terminée. Aucune de celles-ci n'est le cas intéressant en discussion. 

Le cas intéressant est lorsque vous appelez Wait() sur une tâche dans l’état WaitingToRun, ce qui signifie qu’elle a déjà été mise en file d’attente à un TaskScheduler mais que TaskScheduler n’a pas encore réussi à exécuter le délégué de la tâche. Dans ce cas, l'appel à Wait demandera au planificateur s'il est correct d'exécuter la tâche de temps en temps sur le thread actuel, via un appel à la méthode TryExecuteTaskInline du planificateur. Ceci s'appelle inlining. Le planificateur peut choisir d'insérer la tâche en ligne via un appel à base.TryExecuteTask ou il peut renvoyer "false" pour indiquer qu'il n'exécute pas la tâche (cela se fait souvent avec une logique comme ...

return SomeSchedulerSpecificCondition() ? false : TryExecuteTask(task);

La raison pour laquelle TryExecuteTask renvoie un booléen est qu’elle gère la synchronisation pour s’assurer qu’une tâche donnée n’est exécutée qu’une seule fois. Ainsi, si un planificateur veut interdire complètement l'inline de la tâche pendant Wait, elle peut simplement être implémentée en tant que return false; Si un ordonnanceur veut toujours autoriser l'inline chaque fois que possible, elle peut simplement être implémentée comme suit:

return TryExecuteTask(task);

Dans l'implémentation actuelle (.NET 4 et .NET 4.5, et personnellement, je ne m'attends pas à ce que cela change), le planificateur par défaut qui cible le ThreadPool permet l'inline si le thread actuel est un thread ThreadPool et si ce thread était le l'un d'avoir déjà mis la file d'attente en file d'attente. 

Notez qu’il n’ya pas de réentrance arbitraire ici, en ce sens que le planificateur par défaut ne pompera pas de threads arbitraires lorsqu’il attend une tâche ... il ne permettra que cette tâche soit en ligne, et bien sûr, chaque tâche en ligne décide à son tour. faire. Notez également que Wait ne demandera même pas au planificateur dans certaines conditions, préférant bloquer. Par exemple, si vous transmettez CancellationToken annulable, ou si vous transmettez un délai d'expiration non infini, le système n'essaiera pas de s'inscrire en ligne, car l'exécution de la tâche risque de prendre un temps arbitrairement long. est tout ou rien, et cela pourrait retarder considérablement la demande d'annulation ou le délai d'attente. Dans l’ensemble, TPL essaie de trouver ici un bon équilibre entre gaspiller le fil qui fait la variable Wait 'ing et le réutiliser trop souvent. Ce type de mise en ligne est très important pour les problèmes récursifs de division et de conquête (par exemple, QuickSort ), dans lesquels vous créez plusieurs tâches, puis attendez qu'elles soient toutes terminées. Si cela était fait sans alignement, vous auriez très rapidement une impasse, car vous épuiseriez tous les fils du pool et tous les futurs qu'il souhaitait vous donner.

Séparé de Wait, il est également (à distance) possible que l’appel Task.Factory.StartNew finisse par exécuter la tâche à tout moment, si le planificateur utilisé choisit de l’exécuter de manière synchrone dans le cadre de l’appel QueueTask. Aucun des planificateurs intégrés à .NET ne le fera jamais, et je pense personnellement que ce serait une mauvaise conception pour le planificateur, mais c'est théoriquement possible, par exemple:

protected override void QueueTask(Task task, bool wasPreviouslyQueued)
{
    return TryExecuteTask(task);
}

La surcharge de Task.Factory.StartNew qui n’accepte pas une TaskScheduler utilise le planificateur de la TaskFactory, qui dans le cas de Task.Factory vise TaskScheduler.Current. Cela signifie que si vous appelez Task.Factory.StartNew depuis une tâche mise en file d'attente vers cette mythique RunSynchronouslyTaskScheduler, elle sera également mise en file d'attente vers RunSynchronouslyTaskScheduler, ce qui entraînera l'appel StartNew à exécuter la tâche de manière synchrone. Si cela vous préoccupe (par exemple, vous implémentez une bibliothèque et vous ne savez pas d'où vous allez être appelé), vous pouvez explicitement passer TaskScheduler.Default à l'appel StartNew, utilisez - -CODE-- (qui va toujours à Task.Run), ou utilisez une TaskFactory créée pour cibler TaskScheduler.Default.EDIT: D'accord, il semblerait que je me sois complètement trompé et qu'un fil qui attend actuellement une tâche peut soit détourné. Voici un exemple plus simple de ce qui se passe:.


TaskScheduler.Default

Exemple de sortie:

using System;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleApplication1 {
    class Program {
        static void Main() {
            for (int i = 0; i < 10; i++)
            {
                Task.Factory.StartNew(Launch).Wait();
            }
        }

        static void Launch()
        {
            Console.WriteLine("Launch thread: {0}", 
                              Thread.CurrentThread.ManagedThreadId);
            Task.Factory.StartNew(Nested).Wait();
        }

        static void Nested()
        {
            Console.WriteLine("Nested thread: {0}", 
                              Thread.CurrentThread.ManagedThreadId);
        }
    }
}

Comme vous pouvez le constater, le thread en attente est souvent réutilisé pour exécuter la nouvelle tâche. Cela peut se produire même si le thread a acquis un verrou. Nasty ré-entrancy. Je suis convenablement choqué et inquiet :(

Launch thread: 3
Nested thread: 3
Launch thread: 3
Nested thread: 3
Launch thread: 3
Nested thread: 3
Launch thread: 3
Nested thread: 3
Launch thread: 4
Nested thread: 4
Launch thread: 4
Nested thread: 4
Launch thread: 4
Nested thread: 4
Launch thread: 4
Nested thread: 4
Launch thread: 4
Nested thread: 4
Launch thread: 4
Nested thread: 4

As you can see, there are lots of times when the waiting thread is reused to execute the new task. This can happen even if the thread has acquired a lock. Nasty re-entrancy. I am suitably shocked and worried :(

82
Jon Skeet

Pourquoi ne pas simplement concevoir pour cela, plutôt que de se mettre en quatre pour s'assurer que cela ne se produise pas? 

Ici, le TPL est un fouillis rouge, la réentrance peut se produire dans n'importe quel code, à condition que vous puissiez créer un cycle, et vous ne savez pas avec certitude ce qui va se passer au sud de votre cadre de pile. La réentrance synchrone est le meilleur résultat ici - au moins vous ne pouvez pas vous bloquer vous-même (aussi facilement).

Les verrous gèrent la synchronisation multithread. Ils sont orthogonaux à la gestion de la réentrance. Si vous ne protégez pas une ressource à usage unique authentique (probablement un périphérique physique, auquel cas vous devriez probablement utiliser une file d'attente), pourquoi ne pas simplement vous assurer que l'état de votre instance est cohérent afin que la réentrance puisse «fonctionner».

(Côté pensée: les sémaphores sont-ils réentrants sans décrémenter?)

4
piers7

La solution avec new CancellationToken() proposée par Erwin ne fonctionnait pas pour moi, il était tout de même inline.

J'ai donc fini par utiliser une autre condition conseillée par Jon et Stephen (... or if you pass in a non-infinite timeout ...):

  Task<TResult> task = Task.Run(func);
  task.Wait(TimeSpan.FromHours(1)); // Whatever is enough for task to start
  return task.Result;

Remarque: Pour le simplifier, en omettant la gestion des exceptions, etc.

0
Vitaliy Ulantikov

Vous pouvez facilement tester cela en écrivant une application rapide qui partage une connexion de socket entre des threads/tâches.

La tâche obtiendrait un verrou avant d’envoyer un message par le socket et d’attendre une réponse. Une fois que cela se bloque et devient inactif (IOBlock), définissez une autre tâche dans le même bloc pour faire de même. Il doit bloquer lors de l’acquisition du verrou, s’il ne le fait pas et que la deuxième tâche est autorisée à passer le verrou car elle est exécutée par le même thread, vous avez donc un problème.

0
JonPen