C#异步等待永远不会返回到主线程

3
我写了一个非常简单的检查来告诉我我在哪个线程上,然后添加了一些async await代码。我注意到第一次检查时我在thread1上,然后是thread3,而且在我的代码执行期间我从未返回到thread1。
有人能解释一下为什么在await之后我不会返回调用await的主线程吗?WhatThreadAmI()的输出如下:
**********************
Main - 17 -- True
**********************
**********************
CountAsync - 37 -- False
**********************
**********************
Main - 22 -- False
**********************
**********************
Main - 29 -- False
**********************

示例代码:

class Program
{
    static Thread mainThread;
    public static async Task Main(string[] args)
    {
        mainThread = Thread.CurrentThread;


        WhatThreadAmI();

        Console.WriteLine("Counting until 100 million in 5 seconds ...");
        var msWait = await CountAsync();

        WhatThreadAmI();
        Console.WriteLine($"Counting to 100 million took {msWait} milliseconds.");


        Console.WriteLine("Press any key to exit");
        Console.ReadKey();

        WhatThreadAmI();
    }


    static async Task<String> CountAsync()
    {
        return await Task.Run(() =>
        {
            WhatThreadAmI();
            Task.Delay(TimeSpan.FromSeconds(5)).Wait();
            var startTime = DateTime.Now;

            var num = 0;

            while (num < 100000000)
            {
                num += 1;
            }

            return (DateTime.Now - startTime).TotalMilliseconds.ToString();

        });
    }


    static void WhatThreadAmI([CallerMemberName]string Method = "", [CallerLineNumber]int Line = 0)
    {
        const string dividor = "**********************";

        Debug.WriteLine(dividor);
        Debug.WriteLine($"{Method} - {Line} -- {IsMainThread()}");
        Debug.WriteLine(dividor);
    }

    public static bool IsMainThread() => mainThread == Thread.CurrentThread;

}

4
为什么你认为它必须返回那里? - Evk
Await 告诉编译器启动一个线程,跳转到另一个线程(跳过代码并继续执行),然后返回到调用者线程。 - Bailey Miller
我的理解是,状态机被创建了,但我不知道这是否意味着它会返回到原始线程。 - MotKohn
@BaileyMiller 你说的都不是真的,await 告诉编译器:“检查任务是否完成,如果完成了,则在调用线程上运行后续操作。如果没有完成,则使用当前 OperationContext 进行安排,在没有当前 OperationContext 的情况下,使用默认调度程序(即线程池)进行安排。” - Scott Chamberlain
@ScottChamberlain在下面的评论中提到我主要编写Wpf应用程序。这就是我在做这个演示时感到困惑的原因所在。 - Bailey Miller
显示剩余3条评论
2个回答

3
await 会捕获当前的同步上下文 (SynchronizationContext.Current),并将继续执行 (await 后面的所有内容) 发送到该上下文中(除非使用 ConfigureAwait(false))。如果没有同步上下文,比如在控制台应用程序中(您的情况) - 默认情况下,继续执行将被安排到线程池线程。您的主线程不是线程池线程,因此您永远不会在您发布的代码中返回到它。
请注意,每个同步上下文实现都可以决定如何处理发布到它的回调,它不一定要将回调发布到单个线程(如 WPF \ WinForms 同步上下文所做的那样)。因此,即使有同步上下文,也不能保证“返回到调用线程”。

1
我一直以为我回到了我的主线程,或者说本质上是我的UI线程。我主要编写Wpf应用程序,这就是我通常考虑线程的方式。因此,使用我的当前代码,我将从1个线程开始,生成另一个线程,并在我的原始线程休眠时拥有2个线程的生命周期。 - Bailey Miller
@BaileyMiller 是的。使用异步主函数只是一个包装器,用于一个看起来像这样的函数:static void RealMain(string[] args) { Main(args).GetAwaiter().GetResult();} 这样可以实现让“主”线程在整个程序中睡眠,而线程池线程则完成所有工作的行为。 - Scott Chamberlain
@BaileyMiller 在 WPF 中确实会返回到主线程,但那是因为在 WPF 中有特定的“同步上下文”。至于线程-不完全是这样,因为线程池以自己的方式管理线程。它将根据需要创建和销毁线程。Task.Run 不一定会创建新线程,线程池可能已经为您提供了可用的线程。并且长时间未使用的线程将被池销毁。但是您的主线程当然始终存在,在这种情况下大多数时间都处于睡眠状态。 - Evk
1
我从未深入研究过 C# 中 API 如何处理线程,也没有意识到在 C# 应用程序之间存在如此大的差异。感谢您的回答和评论中的讨论。 - Bailey Miller
默认情况下,继续操作将被安排到线程池线程中。-- 这不是它的工作方式。在没有同步上下文的情况下,继续操作将在完成任务的任何线程上运行(演示)。只是碰巧在.NET中,所有内置的异步API都在ThreadPool线程上完成任务,从而产生了这种常见的错觉。 - undefined

0
即使现在可以拥有异步主函数,但控制台应用程序仍然没有操作上下文。因此它将使用默认的任务调度程序,这将导致在await之后的延续使用任何线程池线程而不是与OperationContext相关联的原始线程。

那么,如果我继续使用await并生成更多的线程,本质上我将生成被放置在等待状态的线程,而其他线程正在工作?我的主线程处于WaitSleepJoin ThreadState状态。 - Bailey Miller
唯一“生成”线程的是您代码中的 Task.Run。await 不会创建线程,它只是等待任务进入已完成状态,然后使用 OperationContext 运行 await 后面的代码继续执行(在您的情况下,将工作排队到线程池)。 - Scott Chamberlain

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接