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 ne me soucie pas du thread sur lequel il s’exécute tant qu’il est différent (les informations fournies dans cette question ne sont donc pas utiles).

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

 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: Basé sur 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()); } 

J’ai envoyé un mail à Stephen Toub – membre de l’ équipe PFX – à propos de cette question. Il me revient très rapidement, avec beaucoup de détails – alors je vais copier et coller son texte ici. Je n’ai pas tout cité, car lire un grand nombre de textes cités finit par devenir moins confortable que le noir sur blanc à la vanille, mais en réalité, c’est Stephen – je ne sais pas grand chose 🙂 ce wiki de réponse de la communauté pour refléter que tout le bien ci-dessous n’est pas vraiment mon contenu:

Si vous appelez Wait() sur une tâche terminée, il n’y aura plus de blocage (il ne fera que lancer une exception si la tâche s’est terminée avec un TaskStatus autre que RanToCompletion , ou retourner sous une autre forme). Si vous appelez Wait() sur une tâche en cours d’exécution, celle-ci doit être bloquée car il n’ya rien d’autre à faire (quand je dis bloc, j’inclus à la fois l’attente basée sur le kernel et la rotation). mélange des deux). De même, si vous appelez Wait() sur une tâche ayant l’état Created ou WaitingForActivation , elle bloquera jusqu’à la fin de la tâche. Aucun de ces cas n’est le cas intéressant en cours de 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 queue dans un TaskScheduler, mais que TaskScheduler n’a pas encore exécuté le délégué de la tâche. Dans ce cas, l’appel à Wait demandera au programmateur s’il peut exécuter la tâche-and-there sur le thread en cours, via un appel à la méthode TryExecuteTaskInline du TryExecuteTaskInline . Ceci s’appelle l’ inlining . Le planificateur peut choisir d’inclure la tâche 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 TryExecuteTask laquelle TryExecuteTask renvoie un booléen est qu’il 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 ordonnanceur veut complètement interdire l’incrustation de la tâche en Wait , il peut simplement être implémenté en tant que return false; Si un ordonnanceur veut toujours autoriser l’inclusion chaque fois que possible, il peut simplement être implémenté comme suit:

 return TryExecuteTask(task); 

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

Notez qu’il n’y a pas de réentrance arbitraire ici, dans la mesure où le planificateur par défaut ne pompe pas les threads arbitraires en attendant une tâche … cela permet uniquement d’inclure cette tâche, et bien sûr, toute tâche en cours décide à son tour faire. Notez également que Wait ne demandera même pas au planificateur dans certaines conditions, préférant plutôt bloquer. Par exemple, si vous passez un CancellationToken annulable, ou si vous passez un délai d’expiration non infini, il n’essayera pas de se connecter car cela peut prendre beaucoup de temps pour exécuter l’exécution de la tâche. , et cela pourrait retarder considérablement la demande d’annulation ou le délai d’attente. Dans l’ensemble, TPL essaie de trouver un équilibre satisfaisant entre gaspiller le thread qui effectue l’ Wait et réutiliser ce thread pour trop. Ce type d’inlining est vraiment important pour les problèmes de récursivité diviser-et-conquérir (par exemple QuickSort ) où vous engendrez des tâches multiples et attendez ensuite qu’elles soient toutes terminées. Si tel était le cas sans incrustation, vous vous retrouveriez rapidement dans une impasse lorsque vous épuisiez tous les fils de la piscine et tous ceux qu’elle voulait vous offrir.

Séparé de Wait , il est également possible (à distance) que l’appel Task.Factory.StartNew finisse par exécuter la tâche, si le planificateur utilisé a choisi d’exécuter la tâche de manière synchrone dans le cadre de l’appel QueueTask. Aucun des ordonnanceurs 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 un TaskScheduler utilise le planificateur de TaskFactory , qui dans le cas de Task.Factory cible TaskScheduler.Current . Cela signifie que si vous appelez Task.Factory.StartNew depuis une tâche en queue vers ce mythique RunSynchronouslyTaskScheduler , il est également mis en queue sur RunSynchronouslyTaskScheduler , ce qui entraîne l’appel StartNew exécutant la tâche de manière synchrone. Si cela vous préoccupe (par exemple, si vous implémentez une bibliothèque et que vous ne savez pas d’où vous allez être appelé), vous pouvez explicitement passer TaskScheduler.Default à l’appel StartNew , utilisez Task.Run (qui passe toujours à TaskScheduler.Default ), ou utilisez un TaskFactory créé pour cibler TaskScheduler.Default .


EDIT: OK, il semble que je me suis complètement trompé, et un thread qui attend actuellement une tâche peut être détourné. Voici un exemple plus simple de cela:

 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); } } } 

Sortie de l'échantillon:

 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 

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

Pourquoi ne pas simplement concevoir pour cela, plutôt que de se plier en quatre pour s’assurer que cela n’arrive pas?

Le TPL est un harcèlement rouge ici, 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 frame de stack. La réentrance synchrone est le meilleur résultat ici – au moins vous ne pouvez pas vous autobloquer (aussi facilement).

Les verrous gèrent la synchronisation des threads. Ils sont orthogonaux à la gestion de la réentrance. À moins que vous ne protégiez une ressource à usage unique authentique (probablement un périphérique physique, auquel cas vous devriez probablement utiliser une queue), pourquoi ne pas vous assurer que l’état de votre instance est cohérent et que la réentrée peut simplement fonctionner.

(Côté pensée: les sémaphores sont-ils rentrants sans décrémentation?)

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

La tâche acquiert un verrou avant d’envoyer un message dans le socket et d’attendre une réponse. Une fois que cela bloque et devient inactif (IOBlock), définissez une autre tâche dans le même bloc pour faire la même chose. Il devrait bloquer lors de l’acquisition du verrou, si ce n’est pas le cas et que la deuxième tâche est autorisée à passer le verrou car elle s’exécute par le même thread, alors vous avez un problème.

La solution avec la new CancellationToken() proposée par Erwin n’a pas fonctionné pour moi, mais l’inclusion s’est produite de toute façon.

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 task = Task.Run(func); task.Wait(TimeSpan.FromHours(1)); // Whatever is enough for task to start return task.Result; 

Remarque: en omettant la gestion des exceptions, etc., par souci de simplicité, vous devez vous soucier de ceux du code de production.