为啥不等待 Task.Run() 同步回 UI 线程/原始上下文?

Posted

技术标签:

【中文标题】为啥不等待 Task.Run() 同步回 UI 线程/原始上下文?【英文标题】:Why doesn't await Task.Run() sync back to UI Thread / origin context?为什么不等待 Task.Run() 同步回 UI 线程/原始上下文? 【发布时间】:2020-04-01 04:20:15 【问题描述】:

我以为我理解了 async-wait 模式Task.Run 操作。 但我想知道为什么在下面的代码示例中,await 在完成任务返回后没有同步回 UI 线程。

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

此代码在带有附加 Visual Studio 2019 调试器的 Windows 10 系统上的 .NET Framework WPF 应用程序中运行。 我从 App 类的构造函数中调用此代码。

public App()

    this.InitializeAsync().ConfigureAwait(true);

也许这不是最好的方法,但我不确定这是否是奇怪行为的原因。

代码从 UI 线程开始,应该执行一些任务。 使用await 操作和ConfigureAwait(true) 在任务完成后它应该在主线程(1)上继续。但事实并非如此。

为什么?

【问题讨论】:

@SushantYelpale 不正确 【参考方案1】:

这是一件棘手的事情。

您在 UI 线程上调用 await,这是真的。但!你是在App 的构造函数中做的。

请记住,隐式生成的启动代码如下所示:

public static void Main()

    var app = new YourNamespace.App();
    app.InitializeComponent();
    app.Run();

用于返回主线程的事件循环仅作为Run 执行的一部分启动。所以在App 构造函数运行期间,没有事件循环。然而。

因此,SynchronizationContext,在技术上负责在await 之后将流返回到主线程,在 App 的构造函数中是 null

(SynchronizationContextawait 捕获 before 等待,所以 after 完成 Task 没有关系已经有一个有效的 @987654332 @:捕获的值为null,因此await继续在线程池线程上执行。)

所以问题不在于您在构造函数中运行代码,问题在于您在App 的构造函数中运行它,此时应用程序尚未完全设置好执行。 MainWindow 的构造函数中的相同代码会表现良好。

让我们做一些实验:

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

第一个输出给出

sc = null

第二个

sc = System.Windows.Threading.DispatcherSynchronizationContext

所以你可以看到OnStartup 中已经有一个同步上下文。因此,如果您将InitializeAsync() 移动到OnStartup,它将按照您的预期运行。

【讨论】:

以上是关于为啥不等待 Task.Run() 同步回 UI 线程/原始上下文?的主要内容,如果未能解决你的问题,请参考以下文章

如何启动并等待 Task 运行

在同步方法中运行没有 Wait() 的 Task.Run() 有啥影响?

使用异步方法等待 Task.Run 不会在正确的线程上引发异常

CS0121'Task.Run之间的调用不明确 (Func键 )'和'Task.Run(Func )”

为啥通过调用Task.Run和ThreadPool.QueueUserWorkItem排队到ThreadPool时线程数增加了不止一个?

Task.Factory.StartNew 和 Task.Run