Ich habe Stephen Toub - ein Mitglied des PFX-Teams - über diese Frage informiert . Er ist sehr schnell und mit vielen Details zu mir zurückgekehrt - also kopiere ich einfach seinen Text und füge ihn hier ein. Ich habe nicht alles zitiert, da das Lesen einer großen Menge zitierten Textes weniger bequem ist als Vanille-Schwarz-auf-Weiß, aber das ist wirklich Stephen - ich weiß nicht so viel :) Ich habe es gemacht Dieses Antwort-Community-Wiki zeigt, dass all die Güte unten nicht wirklich mein Inhalt ist:
Wenn Sie Wait()
eine abgeschlossene Aufgabe aufrufen , erfolgt keine Blockierung (es wird nur eine Ausnahme ausgelöstRanToCompletion
, wenn die Aufgabe mit einem anderen TaskStatus als ausgeführt wurde oder auf andere Weise als Nein zurückgegeben wird ). Wenn Sie Wait()
eine Task aufrufen , die bereits ausgeführt wird, muss sie blockiert werden, da es sonst nichts gibt, was sie vernünftigerweise tun kann (wenn ich Block sage, schließe ich sowohl echtes kernelbasiertes Warten als auch Drehen ein, da dies normalerweise eine Mischung aus beiden darstellt ). Wenn Sie Wait()
eine Aufgabe mit dem Status Created
oder aufrufen WaitingForActivation
, wird sie blockiert, bis die Aufgabe abgeschlossen ist. Keiner von diesen ist der interessante Fall, der diskutiert wird.
Der interessante Fall ist, wenn Sie Wait()
eine Aufgabe im WaitingToRun
Status aufrufen , was bedeutet, dass sie zuvor in der Warteschlange eines TaskSchedulers stand , TaskScheduler jedoch noch nicht dazu gekommen ist, den Delegaten der Aufgabe tatsächlich auszuführen. In diesem Fall Wait
fragt der Aufruf von den Scheduler, ob es in Ordnung ist, die Task dann und dort im aktuellen Thread über einen Aufruf der Scheduler- TryExecuteTaskInline
Methode auszuführen . Dies nennt man Inlining . Der Scheduler kann wählen, ob die Aufgabe entweder über einen Aufruf von inline geschaltet werden soll base.TryExecuteTask
, oder er kann 'false' zurückgeben, um anzuzeigen, dass die Aufgabe nicht ausgeführt wird (häufig erfolgt dies mit Logik wie ...
return SomeSchedulerSpecificCondition() ? false : TryExecuteTask(task);
Der Grund, warum TryExecuteTask
ein Boolescher Wert zurückgegeben wird, besteht darin, dass er die Synchronisierung übernimmt, um sicherzustellen, dass eine bestimmte Aufgabe immer nur einmal ausgeführt wird. Wenn ein Scheduler das Inlining der Aufgabe während Wait
dieser Zeit vollständig verbieten möchte , kann es einfach so implementiert werden: return false;
Wenn ein Scheduler das Inlining immer zulassen möchte, wann immer dies möglich ist, kann es einfach wie folgt implementiert werden:
return TryExecuteTask(task);
In der aktuellen Implementierung (sowohl .NET 4 als auch .NET 4.5, und ich persönlich erwarte keine Änderung) ermöglicht der Standard-Scheduler, der auf den ThreadPool abzielt, Inlining, wenn der aktuelle Thread ein ThreadPool-Thread ist und wenn dieser Thread der war eine, die zuvor die Aufgabe in die Warteschlange gestellt hat.
Beachten Sie, dass es hier keine willkürliche Wiedereintrittsmöglichkeit gibt, da der Standardplaner beim Warten auf eine Aufgabe keine willkürlichen Threads pumpt. Er erlaubt nur, dass diese Aufgabe inline ist, und natürlich entscheidet jede Inlining dieser Aufgabe wiederum machen. Beachten Sie Wait
auch, dass der Scheduler unter bestimmten Bedingungen nicht einmal gefragt wird, sondern lieber blockiert wird. Wenn Sie beispielsweise ein stornierbares CancellationToken übergeben oder ein nicht unendliches Timeout übergeben, wird nicht versucht, eine Inline-Funktion auszuführen, da die Inline-Ausführung der Aufgabe beliebig lange dauern kann. Dies ist alles oder nichts Dies könnte die Stornierungsanforderung oder das Zeitlimit erheblich verzögern. Insgesamt versucht TPL hier ein angemessenes Gleichgewicht zwischen der Verschwendung des Threads zu finden, der das tutWait
diesen Thread für zu viel verwenden und wiederverwenden. Diese Art von Inlining ist sehr wichtig für rekursive Divide-and-Conquer-Probleme (z. B. QuickSort ), bei denen Sie mehrere Aufgaben erzeugen und dann warten, bis alle abgeschlossen sind. Wenn dies ohne Inlining geschehen würde, würden Sie sehr schnell zum Stillstand kommen, wenn Sie alle Threads im Pool und alle zukünftigen Threads, die es Ihnen geben wollte, erschöpfen.
Unabhängig davon Wait
ist es auch (remote) möglich, dass der Task.Factory.StartNew- Aufruf die Task dann und dort ausführt, wenn der verwendete Scheduler die Task synchron als Teil des QueueTask-Aufrufs ausführt. Keiner der in .NET integrierten Scheduler wird dies jemals tun, und ich persönlich denke, es wäre ein schlechtes Design für Scheduler, aber es ist theoretisch möglich, z.
protected override void QueueTask(Task task, bool wasPreviouslyQueued)
{
return TryExecuteTask(task);
}
Die Überlastung davon Task.Factory.StartNew
akzeptiert nicht a TaskScheduler
verwendet den Scheduler von der TaskFactory
, was im Fall von Task.Factory
Zielen TaskScheduler.Current
. Dies bedeutet, wenn Sie Task.Factory.StartNew
aus einer Task heraus anrufen, die sich in der Warteschlange zu diesem Mythos befindet RunSynchronouslyTaskScheduler
, wird auch eine Warteschlange angestellt RunSynchronouslyTaskScheduler
, was dazu führt , dass der StartNew
Aufruf die Task synchron ausführt. Wenn Sie darüber überhaupt besorgt sind (z. B. wenn Sie eine Bibliothek implementieren und nicht wissen, von wo aus Sie aufgerufen werden), können Sie explizit TaskScheduler.Default
an den StartNew
Aufruf übergeben, verwenden Task.Run
(was immer geht TaskScheduler.Default
), oder verwenden Sie ein TaskFactory
erstelltes Ziel TaskScheduler.Default
.
EDIT: Okay, es sieht so aus, als hätte ich mich völlig geirrt und ein Thread, der gerade auf eine Aufgabe wartet, kann entführt werden. Hier ist ein einfacheres Beispiel dafür:
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);
}
}
}
Beispielausgabe:
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
Wie Sie sehen, wird der wartende Thread häufig zur Ausführung der neuen Aufgabe wiederverwendet. Dies kann auch dann passieren, wenn der Thread eine Sperre erhalten hat. Böser Wiedereintritt. Ich bin entsprechend schockiert und besorgt :(
StartNew
. Eine Aufgabe ist als asynchrone Operation definiert, die nicht unbedingt einen neuen Thread impliziert. Möglicherweise wird auch irgendwo ein vorhandener Thread oder eine andere asynchrone Methode verwendet.