Por que não espera a sincronização de Task.Run () de volta ao contexto de thread / origem da interface do usuário?

8

Pensei entender o padrão de espera assíncrona e a Task.Runoperação.
Mas estou me perguntando por que, no exemplo de código a seguir, awaitele não é sincronizado novamente com o thread da interface do usuário depois de retornar da tarefa concluída.

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??
}

Esse código é executado em um aplicativo .NET Framework WPF em um sistema Windows 10 com o Visual Studio 2019 Debugger anexado.
Estou chamando esse código do construtor da minha Appclasse.

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

Talvez não seja o melhor caminho, mas não tenho certeza se esse é o motivo do comportamento estranho.

O código começa com o thread da interface do usuário e deve executar algumas tarefas. Com a awaitoperação e ConfigureAwait(true)após a conclusão da tarefa, ela deve continuar no thread principal (1). Mas isso não acontece.

Por quê?

rittergig
fonte
4
@SushantYelpale incorreto
MickyD

Respostas:

10

É uma coisa complicada.

Você está chamando awaitno thread da interface do usuário, é verdade. Mas! Você está fazendo isso dentro Appdo construtor.

Lembre-se de que o código de inicialização gerado implicitamente se parece com o seguinte:

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

O loop de eventos, usado para retornar ao thread principal, é iniciado apenas como parte da Runexecução. Portanto, durante a Appexecução do construtor, não há loop de eventos. Ainda.

Como conseqüência, o SynchronizationContext, que é tecnicamente responsável pelo retorno do fluxo ao encadeamento principal depois await, está nullno construtor do aplicativo.

( SynchronizationContexté capturado await antes de aguardar, portanto, não importa que, depois de terminar Task, já exista um válido SynchronizationContext: o valor capturado é null, portanto, awaitcontinua a execução em um encadeamento do conjunto de encadeamentos.)

Portanto, o problema não é que você esteja executando o código em um construtor; o problema é que você esteja executando-o no Appconstrutor; nesse momento, o aplicativo ainda não está totalmente configurado para execução. O mesmo código no MainWindowconstrutor se comportaria bem.

Vamos fazer um experimento:

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);
}

A primeira saída fornece

sc = null

o segundo

sc = System.Windows.Threading.DispatcherSynchronizationContext

Então você pode ver que já OnStartupexiste um contexto de sincronização. Então, se você se mover InitializeAsync()em OnStartup, ele irá se comportar como você esperaria dele.

Vlad
fonte