75 votes

Est-il garanti que Task.Factory.StartNew() utilise un autre thread que le thread appelant ?

Je lance une nouvelle tâche à partir d'une fonction mais je ne voudrais pas qu'elle s'exécute sur le même fil. Je ne me soucie pas de savoir sur quel thread elle s'exécute, du moment qu'il s'agit d'un thread différent (ainsi, les informations données dans le fichier cette question n'est pas utile).

Ai-je la garantie que le code ci-dessous sortira toujours TestLock avant de permettre Task t pour y entrer à nouveau ? Si ce n'est pas le cas, quel est le modèle de conception recommandé pour empêcher la ré-entrée ?

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, une manière 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());
}

0 votes

Je doute que vous puissiez garantir qu'un nouveau fil sera créé lors de l'appel à StartNew . Une tâche est définie comme une asynchrone ce qui n'implique pas nécessairement la création d'un nouveau fil. Il peut aussi utiliser un thread existant quelque part, ou une autre façon de faire de l'asynchrone.

0 votes

Si vous utilisez C# 5, pensez à remplacer t.Wait() avec await t . Wait ne correspond pas vraiment à la philosophie de TPL.

0 votes

Dans le code donné, le comportement le plus efficace serait qu'elle a fait utiliser le fil d'appel. Ce fil est assis là à ne rien faire après tout.

83voto

Jon Skeet Points 692016

J'ai envoyé un mail à Stephen Toub - un membre de la L'équipe PFX - sur cette question. Il m'a répondu très rapidement, avec beaucoup de détails - je vais donc copier et coller son texte ici. Je ne l'ai pas cité en entier, car lire une grande quantité de texte cité finit par devenir moins confortable que le noir sur blanc, mais vraiment, c'est Stephen - je ne connais pas autant de choses :) J'ai fait de cette réponse un wiki communautaire pour refléter le fait que tout ce qui est ci-dessous n'est pas vraiment mon contenu :

Si vous appelez Wait() sur un Tâche qui est achevée, il n'y aura pas de blocage (une exception sera simplement levée si la tâche s'est achevée avec une valeur TaskStatus autre que RanToCompletion ou de retourner comme un nop ). Si vous appelez Wait() sur une tâche qui est déjà en cours d'exécution, elle doit se bloquer car il n'y a rien d'autre qu'elle puisse raisonnablement faire (quand je dis bloquer, j'inclus à la fois la véritable attente basée sur le noyau et le spinning, car elle fera typiquement un mélange des deux). De même, si vous appelez Wait() sur une tâche qui a le Created ou WaitingForActivation il bloquera jusqu'à ce que la tâche soit terminée. Aucun de ces cas n'est le cas intéressant discuté ici.

Le cas intéressant est celui où vous appelez Wait() sur une tâche dans le WaitingToRun ce qui signifie qu'il a déjà été mis en file d'attente dans un système de gestion de l'information. TaskScheduler mais que le TaskScheduler n'a pas encore pris le temps d'exécuter le délégué de la tâche. Dans ce cas, l'appel à Wait demandera au planificateur s'il est d'accord pour exécuter la tâche à ce moment-là sur le thread actuel, via un appel à la fonction TryExecuteTaskInline méthode. Cette méthode est appelée inlining . Le planificateur peut choisir de mettre la tâche en ligne via un appel à la fonction base.TryExecuteTask Il peut également renvoyer "false" pour indiquer qu'il n'exécute pas la tâche (cela se fait souvent avec une logique du type...).

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

La raison 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 planificateur souhaite interdire complètement l'inlining de la tâche pendant l'exécution de la tâche Wait il peut simplement être mis en œuvre comme return false; Si un planificateur veut toujours autoriser l'inlining lorsque c'est possible, il peut simplement l'implémenter comme suit :

return TryExecuteTask(task);

Dans l'implémentation actuelle (à la fois dans .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 l'inlining si le thread actuel est un thread ThreadPool et si ce thread est celui qui a précédemment mis la tâche en file d'attente.

Notez qu'il n'y a pas de réentrance arbitraire ici, dans la mesure où l'ordonnanceur par défaut ne pompera pas de threads arbitraires lorsqu'il attend une tâche... il n'autorisera que cette tâche à être inlined, et bien sûr tout inlining que cette tâche décide à son tour de faire. Notez également que Wait ne demande même pas au planificateur dans certaines conditions, préférant bloquer. Par exemple, si vous passez une commande annulable CancellationToken Si vous passez un délai d'attente non infini, il n'essaiera pas de mettre en ligne car cela pourrait prendre un temps arbitrairement long pour mettre en ligne l'exécution de la tâche, ce qui est tout ou rien, et cela pourrait retarder de manière significative la demande d'annulation ou le délai d'attente. Dans l'ensemble, TPL essaie de trouver un bon équilibre entre le fait de gaspiller le thread qui exécute la tâche et le fait de ne pas l'utiliser. Wait et de réutiliser ce fil pour trop de choses. Ce type d'inlining est vraiment important pour les problèmes récursifs de type "diviser pour régner" (ex. QuickSort ) où l'on génère plusieurs tâches et où l'on attend qu'elles se terminent toutes. Si cela était fait sans inlining, vous vous retrouveriez très rapidement dans une impasse, car vous épuiseriez tous les threads du pool et tous les futurs threads qu'il voudrait vous donner.

Séparé de Wait il est également (vaguement) possible que l'équipe de la Task.Factory.StartNew pourrait finir par exécuter la tâche à ce moment précis, si l'ordonnanceur 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 fera jamais cela, et je pense personnellement que ce serait une mauvaise conception pour un ordonnanceur, 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 l TaskFactory qui, dans le cas de Task.Factory cibles TaskScheduler.Current . Cela signifie que si vous appelez Task.Factory.StartNew à partir d'une tâche en file d'attente vers ce mythique RunSynchronouslyTaskScheduler il ferait également la queue pour RunSynchronouslyTaskScheduler ce qui donne le résultat suivant StartNew appel 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 la commande TaskScheduler.Default à la StartNew appeler, utiliser Task.Run (qui va toujours à TaskScheduler.Default ), ou utiliser un TaskFactory créé pour cibler TaskScheduler.Default .


EDIT : Ok, il semble que je me sois complètement trompé, et qu'un fil qui est actuellement en attente d'une tâche peut être détourné. Voici un exemple plus simple de ce qui se passe :

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

Exemple de sortie :

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, il arrive souvent que le thread en attente soit réutilisé pour exécuter la nouvelle tâche. Cela peut se produire même si le thread a acquis un verrou. Méchante ré-entrance. Je suis choqué et inquiet :(

1 votes

Dans le cas d'un seul thread disponible, son code se bloquerait parce que "après que le code utilisant ce thread ait déjà fini avec lui" n'arrive jamais.

0 votes

Qu'en est-il de l'inlining des tâches ? La tâche peut "détourner" le thread lorsqu'elle appelle Wait() Non ?

0 votes

@svick : Je ne sais pas. pensez à donc. Je serais certainement surpris s'il en était ainsi, et je considérerais cela comme une rupture. Je pense que l'équipe PFX se méfie suffisamment de la réentrance pour empêcher cela :)

4voto

piers7 Points 1567

Pourquoi ne pas concevoir en conséquence, plutôt que de se plier en quatre pour que cela n'arrive pas ?

La TPL est un hareng 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 cadre de pile. La réentrance synchrone est le meilleur résultat ici - au moins vous ne pouvez pas vous auto-débloquer (aussi facilement).

Les verrous gèrent la synchronisation entre les fils. Ils sont orthogonaux à la gestion de la réentrance. À moins que vous ne protégiez une véritable ressource à usage unique (probablement un dispositif physique, auquel cas vous devriez probablement utiliser une file d'attente), pourquoi ne pas vous assurer que l'état de votre instance est cohérent afin que la réentrance puisse "fonctionner".

(Réflexion secondaire : les sémaphores sont-ils réentrants sans décrémentation ?)

0voto

JonPen Points 1

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

La tâche doit acquérir un verrou avant d'envoyer un message sur la socket et d'attendre une réponse. Une fois que cette tâche se bloque et devient inactive (IOBlock), une autre tâche dans le même bloc doit faire de même. Si ce n'est pas le cas et que la deuxième tâche est autorisée à passer le verrou parce qu'elle est exécutée par le même thread, vous avez un problème.

0voto

Vitaliy Ulantikov Points 2834

Solution avec new CancellationToken() proposée par Erwin n'a pas fonctionné pour moi, l'inlining s'est quand même produit.

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;

Note : Nous omettons ici la gestion des exceptions pour des raisons de simplicité, mais vous devriez y prêter attention dans le code de production.

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