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


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.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.