Warum wartet Task.Run () nicht auf die Synchronisierung mit dem UI-Thread / Ursprungskontext?

8

Ich dachte, ich hätte das asynchrone Wartemuster und die Task.RunOperation verstanden.
Ich frage mich jedoch, warum im folgenden Codebeispiel die awaitSynchronisierung nach der Rückkehr von der abgeschlossenen Aufgabe nicht wieder mit dem UI-Thread synchronisiert wird.

public async Task InitializeAsync()
{
    Console.WriteLine($"Thread: {Thread.CurrentThread.ManagedThreadId}"); // "Thread: 1"
    double value = await Task.Run(() =>
    {
        Console.WriteLine($"Thread: {Thread.CurrentThread.ManagedThreadId}"); // Thread: 6

        // Do some CPU expensive stuff
        double x = 42;
        for (int i = 0; i < 100000000; i++)
        {
            x += i - Math.PI;
        }
        return x;
    }).ConfigureAwait(true);
    Console.WriteLine($"Result: {value}");
    Console.WriteLine($"Thread: {Thread.CurrentThread.ManagedThreadId}"); // Thread: 6  - WHY??
}

Dieser Code wird in einer .NET Framework WPF-Anwendung auf einem Windows 10-System mit angeschlossenem Visual Studio 2019-Debugger ausgeführt.
Ich rufe diesen Code vom Konstruktor meiner AppKlasse auf.

public App()
{
    this.InitializeAsync().ConfigureAwait(true);
}

Vielleicht ist es nicht der beste Weg, aber ich bin mir nicht sicher, ob dies der Grund für das seltsame Verhalten ist.

Der Code beginnt mit dem UI-Thread und sollte einige Aufgaben ausführen. Mit der awaitOperation und ConfigureAwait(true)nach Beendigung der Aufgabe sollte sie auf dem Haupt-Thread (1) fortgesetzt werden. Aber das tut es nicht.

Warum?

rittergig
quelle
4
@SushantYelpale falsch
MickyD

Antworten:

10

Es ist eine schwierige Sache.

Sie rufen den awaitUI-Thread auf, das stimmt. Aber! Sie machen es im AppKonstruktor.

Denken Sie daran, dass der implizit generierte Startcode folgendermaßen aussieht:

public static void Main()
{
    var app = new YourNamespace.App();
    app.InitializeComponent();
    app.Run();
}

Die Ereignisschleife, die zum Zurückkehren zum Hauptthread verwendet wird, wird nur als Teil der RunAusführung gestartet . Während des AppKonstruktorlaufs gibt es also keine Ereignisschleife. Noch.

Infolgedessen befindet sich der SynchronizationContext, der technisch für die Rückgabe des Flusses an den Haupt-Thread nach verantwortlich awaitist, nullbeim Konstruktor der App.

( SynchronizationContextWird von await vor dem Warten erfasst , es spielt also keine Rolle, dass nach Abschluss des TaskVorgangs bereits ein gültiger SynchronizationContextWert vorhanden ist: Der erfasste Wert ist null, setzt also awaitdie Ausführung in einem Thread-Pool-Thread fort.)

Das Problem ist also nicht, dass Sie den Code in einem Konstruktor ausführen, sondern dass Sie ihn im Konstruktor des AppKonstruktors ausführen. Zu diesem Zeitpunkt ist die Anwendung noch nicht vollständig für die Ausführung eingerichtet. Der gleiche Code im MainWindowKonstruktor würde sich gut verhalten.

Lassen Sie uns ein Experiment machen:

public App()
{
    Console.WriteLine($"sc = {SynchronizationContext.Current?.ToString() ?? "null"}");
}

protected override void OnStartup(StartupEventArgs e)
{
    Console.WriteLine($"sc = {SynchronizationContext.Current?.ToString() ?? "null"}");
    base.OnStartup(e);
}

Die erste Ausgabe gibt

sc = null

der Zweite

sc = System.Windows.Threading.DispatcherSynchronizationContext

Sie sehen also, dass bereits OnStartupein Synchronisationskontext vorhanden ist. Also , wenn Sie sich bewegen InitializeAsync()in OnStartup, wird es sich verhalten , wie Sie es erwarten würden.

Vlad
quelle