¿Por qué no espera la sincronización de Task.Run () al contexto de UI Thread / origin?

8

Pensé que entendía el patrón de espera asíncrona y la Task.Runoperación.
Pero me pregunto por qué en el siguiente ejemplo de código awaitno se sincroniza con el hilo de la interfaz de usuario después de regresar de la tarea finalizada.

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

Este código se ejecuta dentro de una aplicación .NET Framework WPF en un sistema Windows 10 con el depurador de Visual Studio 2019 adjunto.
Estoy llamando a este código desde el constructor de mi Appclase.

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

Quizás no sea la mejor manera, pero no estoy seguro de si esta es la razón del comportamiento extraño.

El código comienza con el hilo de la interfaz de usuario y debe hacer alguna tarea. Con la awaitoperación y una vez ConfigureAwait(true)finalizada la tarea, debe continuar en el subproceso principal (1). Pero no lo hace.

¿Por qué?

rittergig
fuente
44
@SushantYelpale incorrecto
MickyD

Respuestas:

10

Es algo complicado.

Estás llamando awaital hilo de la interfaz de usuario, es cierto. ¡Pero! Lo estás haciendo dentro Appdel constructor.

Recuerde que el código de inicio generado implícitamente se ve así:

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

El bucle de eventos, que se utiliza para volver al subproceso principal, se inicia solo como parte de la Runejecución. Entonces, durante la Appejecución del constructor, no hay un bucle de eventos. Todavía.

Como consecuencia, el SynchronizationContext, que es técnicamente responsable del retorno del flujo al hilo principal después await, está nullen el constructor de la aplicación.

( SynchronizationContextse captura await antes de esperar, por lo que no importa que después de finalizar Taskya haya un valor válido SynchronizationContext: el valor capturado es null, por lo que awaitcontinúa la ejecución en un grupo de subprocesos).

Entonces, el problema no es que esté ejecutando el código en un constructor, sino que lo está ejecutando en el Appconstructor de S, momento en el cual la aplicación aún no está totalmente configurada para su ejecución. El mismo código en MainWindowel constructor se comportaría bien.

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

El primer resultado da

sc = null

el segundo

sc = System.Windows.Threading.DispatcherSynchronizationContext

Entonces puede ver que ya OnStartuphay un contexto de sincronización. Así que si usted se mueve InitializeAsync()hacia OnStartup, se comportará como se espera también.

Vlad
fuente