Warum ist TaskScheduler.Current der Standard TaskScheduler?
Die Task Parallel Library ist toll und ich habe es verwendet eine Menge in den letzten Monaten. Allerdings gibt es etwas wirklich stört mich: die Tatsache, dass Aufgabenplaner.Current
ist die Standard-task-scheduler, nicht Aufgabenplaner.Default
. Das ist absolut nicht offensichtlich auf den ersten Blick in die Dokumentation noch Proben.
Current
führen kann, zu subtilen bugs, da Ihr Verhalten ändert sich je nachdem, ob Sie in einer anderen Aufgabe. Das kann nicht leicht ermittelt werden.
Nehmen wir an ich Schreibe eine Bibliothek für asynchrone Methoden, mit der die standard async-pattern, basierend auf Ereignisse zu signalisieren Abschluss auf der original-synchronisation Kontext, in genau der gleichen Weise XxxAsync-Methode in der .NET Framework (z.B. DownloadFileAsync
). Ich entscheiden, zu verwenden der Task Parallel Library für die Umsetzung, weil es wirklich einfach zu implementieren dieses Verhalten mit dem folgenden code:
public class MyLibrary {
public event EventHandler SomeOperationCompleted;
private void OnSomeOperationCompleted() {
var handler = SomeOperationCompleted;
if (handler != null)
handler(this, EventArgs.Empty);
}
public void DoSomeOperationAsync() {
Task.Factory
.StartNew
(
() => Thread.Sleep(1000) //simulate a long operation
, CancellationToken.None
, TaskCreationOptions.None
, TaskScheduler.Default
)
.ContinueWith
(t => OnSomeOperationCompleted()
, TaskScheduler.FromCurrentSynchronizationContext()
);
}
}
Bisher klappt alles gut. Nun, lassen Sie uns einen Anruf tätigen, um diese Bibliothek auf eine Schaltfläche klicken Sie in einer WPF oder WinForms-Anwendung:
private void Button_OnClick(object sender, EventArgs args) {
var myLibrary = new MyLibrary();
myLibrary.SomeOperationCompleted += (s, e) => DoSomethingElse();
myLibrary.DoSomeOperationAsync();
}
private void DoSomethingElse() {
...
Task.Factory.StartNew(() => Thread.Sleep(5000)/*simulate a long operation*/);
...
}
Hier, die person schriftlich die Bibliothek nennen, entschied sich für den start einer neuen Task
wenn der Vorgang abgeschlossen ist. Nichts ungewöhnliches. Er oder Sie folgt Beispiele finden sich überall auf dem web und verwenden Sie einfach Task.Factory.StartNew
ohne Angabe der TaskScheduler
(und es gibt keine einfache überlastung angeben, um den zweiten parameter). Die DoSomethingElse
Methode funktioniert gut, wenn man allein ist, aber sobald es aufgerufen wird von der Veranstaltung, die UI friert seit TaskFactory.Current
wird die Wiederverwendung der synchronization context task scheduler aus meiner Bibliothek Fortsetzung.
Herauszufinden, dies könnte einige Zeit dauern, vor allem, wenn Sie die zweite Aufgabe Aufruf begraben unten einige komplexere Aufruf-stack. Natürlich, die Lösung ist einfach, wenn man weiß, wie alles funktioniert: immer angeben TaskScheduler.Default
für jede operation, die Sie erwarten sind, um auf die thread-pool. Aber vielleicht ist die zweite Aufgabe gestartet wird von einem anderen externen Bibliothek, nicht zu wissen, über dieses Verhalten und naiv mit StartNew
ohne einen konkreten scheduler. Ich gehe davon aus, dass dieser Fall ziemlich Häufig.
Nach dem wickeln meinem Kopf herum, die ich nicht verstehen kann, ist die Wahl des Teams schreiben die TPL zu verwenden TaskScheduler.Current
statt TaskScheduler.Default
als Standard:
- Es ist nicht unbedingt offensichtlich,
Default
ist nicht der Standard! Und die Dokumentation ist ernst fehlt. - Die eigentliche task-scheduler verwendet
Current
hängt von den call-stack! Es ist schwer zu pflegen Invarianten mit diesem Verhalten. - Es ist umständlich, geben Sie den task-scheduler mit
StartNew
da müssen Sie die task-Optionen für die Erstellung und Stornierung token erste, was zu lang ist, weniger lesbare Zeilen. Diese gelindert werden können durch das schreiben einer extension-Methode oder das erstellen einerTaskFactory
verwendetDefault
. - Erfassung der call-stack ist eine zusätzliche Leistung Kosten.
- Wenn ich wirklich wollen, eine Aufgabe, abhängig zu sein, auf den anderen Elternteil laufende Aufgabe, die ich lieber angeben, es ausdrücklich zu erleichtern, code zu Lesen, anstatt verlassen sich auf call-stack-magic.
Ich weiß, diese Frage klingt vielleicht sehr subjektiv, aber ich kann nicht finden, eine gute, Objektive Argumente, warum dieses Verhalten so ist, wie es ist. Ich bin sicher, ich bin hier etwas fehlt: das ist, warum drehe ich mich zu dir.
InformationsquelleAutor der Frage Julien Lebosquain | 2011-07-23
Du musst angemeldet sein, um einen Kommentar abzugeben.
Ich denke, dass das aktuelle Verhalten Sinn macht. Wenn ich meinen eigenen task-scheduler, und starten eine Aufgabe, die beginnt, andere Aufgaben auszuführen, möchte ich wahrscheinlich alle Aufgaben zu nutzen, den Planer, den ich schuf.
Ich Stimme zu, dass es seltsam ist, dass manchmal beginnt eine Aufgabe aus der UI-thread verwendet, der Standard-scheduler und manchmal nicht. Aber ich weiß nicht, wie würde ich das besser, wenn ich es entwerfen.
Bezug auf Ihre spezifischen Probleme:
new Task(lambda).Start(scheduler)
. Dies hat den Nachteil, dass Sie angeben müssen, geben argument, wenn die Aufgabe etwas zurückgibt.TaskFactory.Create
ableiten kann, die Art für Sie.Dispatcher.Invoke()
anstattTaskScheduler.FromCurrentSynchronizationContext()
.InformationsquelleAutor der Antwort svick
[BEARBEITEN]
Das folgende behandelt das problem nur mit dem scheduler verwendet
Task.Factory.StartNew
.Allerdings
Task.ContinueWith
hat einen hartcodiertenTaskScheduler.Current
.[/EDIT]
Ersten, gibt es eine einfache Lösung zur Verfügung - siehe unten auf dieser post.
Den Grund für dieses problem ist einfach: Es gibt nicht nur einen Standard-task-scheduler (
TaskScheduler.Default
), aber auch mit einer default-task-scheduler für eineTaskFactory
(TaskFactory.Scheduler
).Diese default-scheduler angegeben werden kann in den Konstruktor der
TaskFactory
wenn er erstellt wird.Jedoch die
TaskFactory
hinterTask.Factory
wird wie folgt erstellt:Wie Sie sehen können, keine
TaskFactory
angegeben ist;null
für die Standard-Konstruktor - besser wäreTaskScheduler.Default
(die Dokumentation besagt, dass "Aktuelle" verwendet wird, das hat die gleichen Folgen).Dies führt wiederum zu der Umsetzung der
TaskFactory.DefaultScheduler
(private member):Hier finden Sie sollten in der Lage sein zu erkennen, den Grund für dieses Verhalten: Als Aufgabe.Die Fabrik hat keine Standard-task-scheduler, wird die aktuelle verwendet.
Also, warum nicht wir laufen in
NullReferenceExceptions
dann, wenn kein Task gerade ausgeführt wird (d.h. wir haben keine aktuelle Aufgabenplaner)?Der Grund ist einfach:
TaskScheduler.Current
standardmäßigTaskScheduler.Default
.Würde ich dies eine sehr unglückliche Umsetzung.
Gibt es jedoch eine einfache Lösung zur Verfügung: Wir können einfach die Standard -
TaskScheduler
vonTask.Factory
zuTaskScheduler.Default
Ich hoffe ich konnte helfen mit meiner Antwort, obwohl es schon sehr spät 🙂
InformationsquelleAutor der Antwort Matthias
Statt
Task.Factory.StartNew()
erwägen:
Task.Run()
Dieser wird immer ausgeführt auf einem threadpool-thread. Ich hatte gerade das gleiche problem in der Frage beschriebenen und ich denke, das ist eine gute Art des Umgangs mit diesen.
Finden Sie in diesem blog-Eintrag:
http://blogs.msdn.com/b/pfxteam/archive/2011/10/24/10229468.aspx
InformationsquelleAutor der Antwort testalino
Default
ist die Standardeinstellung, aber es ist nicht immer dieCurrent
.Als andere haben schon geantwortet, wenn Sie eine Aufgabe ausführen, auf die thread-pool, müssen Sie explizit den
Current
scheduler durch die übergabe derDefault
scheduler entweder in derTaskFactory
oder dieStartNew
Methode.Da Ihre Frage beinhaltete eine Bibliothek, aber ich denke, die Antwort ist, dass sollten Sie nicht tun, ändert sich die
Current
scheduler, der gesehen hat, die von code außerhalb Ihrer Bibliothek. Das bedeutet, dass Sie, sollten Sie nicht verwendenTaskScheduler.FromCurrentSynchronizationContext()
beim anheben derSomeOperationCompleted
Veranstaltung. Stattdessen machen Sie etwas wie dieses:Ich nicht einmal denken, Sie brauchen, um explizit starten Sie Ihre Aufgabe auf die
Default
scheduler - lassen Sie den Anrufer ermitteln dieCurrent
scheduler, wenn Sie wollen.InformationsquelleAutor der Antwort Rory MacLeod
Ich habe gerade Stunden damit verbracht, zu versuchen zu Debuggen ein sonderbares Problem wo meine Aufgabe war es geplant auf dem UI-thread, auch wenn ich es nicht geben. Es stellte sich heraus, das problem war genau das, was dein Beispielcode demonstriert: Eine Aufgabe, die Fortsetzung war geplant auf dem UI-thread, und irgendwo in dieser Fortsetzung, die eine neue Aufgabe wurde gestartet und hab dann auf die geplanten UI-thread, weil die derzeit ausgeführte Aufgabe hatte, ein bestimmtes
TaskScheduler
gesetzt.Zum Glück, es ist alles code, den ich besitze, so kann ich es beheben, indem Sie sicher, dass mein code angeben
TaskScheduler.Default
beim starten von neuen Aufgaben, aber wenn du nicht so viel Glück, mein Vorschlag wäre, die NutzungDispatcher.BeginInvoke
anstatt die UI-scheduler.Statt:
Versuchen:
Es ist ein bisschen weniger lesbar, aber.
InformationsquelleAutor der Antwort René