web-dev-qa-db-de.com

Wird Task.Factory.StartNew () garantiert ein anderer Thread als der aufrufende Thread verwendet?

Ich starte eine neue Aufgabe von einer Funktion aus, möchte aber nicht, dass sie im selben Thread läuft. Es ist mir egal, auf welchem ​​Thread er läuft, solange er ein anderer ist (also die Informationen in diese Frage helfen nicht).

Bin ich garantiert, dass der untenstehende Code immer TestLock beendet, bevor Task t die erneute Eingabe zulässt? Wenn nicht, welches ist das empfohlene Entwurfsmuster, um Wiedereintritt zu verhindern?

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: Basierend auf der untenstehenden Antwort von Jon Skeet und Stephen Toub, besteht eine einfache Möglichkeit zur deterministischen Verhinderung von Wiedereintritt darin, ein CancellationToken zu übergeben.

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

Ich habe Stephen Toub - ein Mitglied des PFX-Teams - über diese Frage informiert. Er ist sehr schnell 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 von zitiertem Text weniger angenehm wird als das von Vanilla Schwarz auf Weiß, aber wirklich, das ist Stephen - ich kenne nicht so viele Sachen :), die ich gemacht habe Dieses Antwort-Community-Wiki soll widerspiegeln, dass all die Güte unten nicht wirklich mein Inhalt ist:

Wenn Sie Wait() für eine abgeschlossene Task aufrufen, wird keine Blockierung durchgeführt (es wird nur eine Ausnahme ausgelöst, wenn die Task mit einem TaskStatus) abgeschlossen wurde anders als RanToCompletion oder auf andere Weise als nop ) zurückgeben. Wenn Sie Wait() für eine Task aufrufen, die bereits ausgeführt wird, muss sie blockieren, da es sonst nichts gibt, was sie vernünftigerweise tun kann beide). Wenn Sie Wait() für eine Task aufrufen, die den Status Created oder WaitingForActivation hat, wird die Task gesperrt, bis die Task abgeschlossen ist. Keiner von diesen ist der interessante Fall, der diskutiert wird.

Der interessante Fall ist, wenn Sie Wait() für eine Task im Status WaitingToRun aufrufen. Dies bedeutet, dass sie zuvor in eine Warteschlange für einen TaskScheduler gestellt wurde, TaskScheduler jedoch noch nicht dazu gekommen ist, die Task tatsächlich auszuführen noch delegieren. In diesem Fall fragt der Aufruf von Wait den Scheduler, ob es in Ordnung ist, die Task dann und dort auf dem aktuellen Thread über einen Aufruf der TryExecuteTaskInline -Methode des Schedulers auszuführen. Dies wird als Inlining bezeichnet. Der Scheduler kann entweder die Aufgabe über einen Aufruf von base.TryExecuteTask einbinden oder 'false' zurückgeben, um anzuzeigen, dass die Aufgabe nicht ausgeführt wird (dies geschieht häufig mit Logik wie ...

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

Der Grund, warum TryExecuteTask einen Booleschen Wert zurückgibt, besteht darin, dass die Synchronisation so gehandhabt wird, dass sichergestellt ist, dass eine bestimmte Task immer nur einmal ausgeführt wird. Wenn ein Scheduler das Inlining der Task während Wait vollständig unterbinden möchte, kann er einfach als return false; implementiert werden. Wenn ein Scheduler immer Inlining zulassen möchte, wann immer dies möglich ist, kann er einfach als:

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, das Inlinen, wenn der aktuelle Thread ein ThreadPool-Thread ist und dieser Thread der Thread war eine, die die Aufgabe zuvor in die Warteschlange gestellt hat.

Beachten Sie, dass es hier keine willkürliche Wiedereintrittsrate gibt, da der Standard-Scheduler beim Warten auf eine Aufgabe keine willkürlichen Threads pumpt. Er lässt nur das Inlinen dieser Aufgabe zu, und natürlich entscheidet jedes Inlinen dieser Aufgabe machen. Beachten Sie auch, dass Wait den Scheduler unter bestimmten Umständen nicht einmal fragt, sondern lieber blockiert. Wenn Sie beispielsweise ein abbrechbares CancellationToken übergeben oder eine nicht unendliche Zeitüberschreitung angeben, wird das Inline-Verfahren nicht versucht, da das Inline-Verfahren der Aufgabe möglicherweise beliebig lange dauern kann Ausführung, die alles oder nichts ist, und die die Stornierungsanforderung oder das Timeout erheblich verzögern könnte. Insgesamt versucht TPL hier ein vernünftiges Gleichgewicht zwischen der Verschwendung des Threads, der das Wait macht, und der zu häufigen Wiederverwendung dieses Threads herzustellen. Diese Art von Inlining ist wirklich wichtig für rekursive Divide-and-Conquer-Probleme (z. B. QuickSort ), bei denen Sie mehrere Aufgaben erzeugen und dann darauf warten, dass alle abgeschlossen sind. Wenn dies ohne Inlining geschehen wäre, würden Sie sehr schnell blockieren, da Sie alle Threads im Pool und alle zukünftigen Threads, die es Ihnen geben wollte, erschöpfen.

Getrennt von Wait ist es auch (remote) möglich, dass der Aufruf Task.Factory.StartNew die Aufgabe dann und dort ausführt, wenn der verwendete Scheduler die Aufgabe synchron ausführt als Teil des QueueTask-Aufrufs. 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 Überladung von Task.Factory.StartNew, die kein TaskScheduler akzeptiert, verwendet den Scheduler aus TaskFactory, der im Fall von Task.Factory auf TaskScheduler.Current abzielt. Dies bedeutet, wenn Sie Task.Factory.StartNew aus einer Task aufrufen, die sich in der Warteschlange für dieses mythische RunSynchronouslyTaskScheduler befindet, wird auch RunSynchronouslyTaskScheduler in die Warteschlange gestellt, wodurch der Aufruf StartNew die Task synchron ausführt. Wenn Sie sich darüber Sorgen machen (z. B. wenn Sie eine Bibliothek implementieren und nicht wissen, woher Sie aufgerufen werden), können Sie TaskScheduler.Default explizit an StartNew übergeben. Rufen Sie an, verwenden Sie Task.Run (der immer zu TaskScheduler.Default wechselt), oder verwenden Sie ein TaskFactory, das als Ziel für TaskScheduler.Default erstellt wurde.


EDIT: Okay, es sieht so aus, als ob ich völlig falsch gelegen hätte 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 in vielen Fällen zum Ausführen der neuen Aufgabe erneut verwendet. Dies kann auch passieren, wenn der Thread eine Sperre erhalten hat. Böse Wiedereintritt. Ich bin angemessen geschockt und besorgt :(

83
Jon Skeet

Warum nicht einfach dafür entwerfen, anstatt sich nach hinten zu beugen, um sicherzustellen, dass es nicht passiert? 

Die TPL ist hier ein roter Hering. Wiedereintritt kann in jedem Code erfolgen, vorausgesetzt, Sie können einen Zyklus erstellen, und Sie wissen nicht genau, was im Süden Ihres Stack-Frames passieren wird. Synchrones Wiedereintritt ist hier das beste Ergebnis - zumindest können Sie sich nicht selbst blockieren (so leicht).

Sperren verwalten die Cross-Thread-Synchronisation. Sie sind orthogonal zur Steuerung der Wiedereintritt. Wenn Sie nicht eine echte Einzelverwendungsressource schützen (wahrscheinlich ein physisches Gerät. In diesem Fall sollten Sie wahrscheinlich eine Warteschlange verwenden), warum sollten Sie nicht einfach sicherstellen, dass Ihr Instanzstatus konsistent ist, damit Wiedereintritt "nur funktionieren" kann.

(Nebengedanke: sind Semaphoren wiedereintrittsfähig ohne zu dekrementieren?)

4
piers7

Die von Erwin vorgeschlagene Lösung mit new CancellationToken() hat bei mir nicht funktioniert, das Inlining kam trotzdem vor.

Also habe ich am Ende eine andere von Jon und Stephen empfohlene Bedingung verwendet. (... 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;

Hinweis: Ausnahmeregelungen usw. werden hier aus Gründen der Vereinfachung ausgelassen. Beachten Sie die Angaben im Produktionscode.

0

Sie können dies leicht testen, indem Sie eine schnelle App schreiben, die eine Socket-Verbindung zwischen Threads/Aufgaben freigibt.

Der Task erhält eine Sperre, bevor er eine Nachricht über den Socket sendet und auf eine Antwort wartet. Sobald dies blockiert und inaktiv ist (IOBlock), setzen Sie eine andere Aufgabe in demselben Block, um dasselbe zu tun. Es sollte blockieren, wenn die Sperre erworben wird. Wenn dies nicht der Fall ist und die zweite Task die Sperre passieren darf, weil sie von demselben Thread ausgeführt wird, liegt ein Problem vor.

0
JonPen