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 :(
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()
avecawait 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.
2 votes
Oui, c'est vrai, et cela ne me dérangerait pas dans ce cas précis. Mais je préfère un comportement déterministe.
2 votes
Si vous utilisez un planificateur qui n'exécute les tâches que sur un thread spécifique, alors non, la tâche ne peut pas être exécuté sur un fil différent. Il est assez courant d'utiliser un
SynchronizationContext
pour s'assurer que les tâches sont exécutées sur le thread de l'interface utilisateur. Si vous exécutez le code qui appelleStartNew
sur le thread de l'interface utilisateur comme ça, alors ils s'exécuteraient tous les deux sur le même thread. La seule garantie est que la tâche sera exécutée de manière asynchrone à partir de l'interface utilisateur.StartNew
(du moins, si vous ne fournissez pas un fichierRunSynchronously
drapeau.8 votes
Si vous voulez "forcer" la création d'un nouveau fil, utilisez l'option "TaskCreationOptions.LongRunning", par exemple :
Task.Factory.StartNew(() => { this.Test(stop: true); }, TaskCreationOptions.LongRunning);
C'est une bonne idée si vos verrous peuvent mettre le thread en état d'attente pendant une période prolongée.0 votes
Erwin, si vous voulez vraiment fonctionner sur des threads séparés, ne serait-il pas préférable de créer et d'utiliser les threads explicitement ?
0 votes
@stic : La seule exigence est de s'exécuter sur un autre thread, ce serait donc inutile car le ThreadPool (notamment via la bibliothèque Task) est bien capable de gérer cela correctement.
0 votes
Pourquoi le contrôle "if(stop) return" se trouve-t-il dans le verrou ? Ce bool est local, pas besoin de le protéger en lecture (sûrement).
0 votes
@piers7 : L'idée était d'empêcher la sortie de la fonction (qui n'est qu'un exemple d'action) jusqu'à ce que le verrou puisse être obtenu. Dans une situation réelle, vous auriez raison de déplacer cette déclaration avant le verrou.
0 votes
La solution de @PeterRitchie a fonctionné pour moi :)
0 votes
LongRunningTask ne garantit PAS l'utilisation d'un nouveau thread, mais fournit simplement un indice à TPL. Un nouveau thread peut être utilisé, mais ce n'est pas une garantie. Comme l'illustre la bonne réponse, seul CancellationToken le fait.