async Main() 中的等待行为令人困惑

2023-11-22

我正在通过 Andrew Troelsen 的书“Pro C# 7 With .NET and .NET Core”学习 C#。在第 19 章(异步编程)中,作者使用了以下示例代码:

        static async Task Main(string[] args)
        {
            Console.WriteLine(" Fun With Async ===>");             
            string message = await DoWorkAsync();
            Console.WriteLine(message);
            Console.WriteLine("Completed");
            Console.ReadLine();
        }
     
        static async Task<string> DoWorkAsync()
        {
            return await Task.Run(() =>
            {
                Thread.Sleep(5_000);
                return "Done with work!";
            });
        }

然后作者指出

“...这个关键字(await)将始终修改返回任务对象的方法。当逻辑流到达等待标记时,调用线程将在此方法中挂起,直到调用完成。如果您要运行此版本的应用程序,您会发现“已完成”消息显示在“完成工作!”之前。信息。如果这是一个图形应用程序,则用户可以在执行 DoWorkAsync() 方法时继续使用 UI”。

但是当我在 VS 中运行这段代码时,我没有得到这种行为。主线程实际上被阻塞了 5 秒,并且直到“完成工作!”之后才显示“已完成”。

浏览有关 async/await 如何工作的各种在线文档和文章,我认为“await”会起作用,例如当遇到第一个“await”时,程序检查该方法是否已经完成,如果没有,它会立即“ return”到调用方法,然后在可等待任务完成后返回。

But 如果调用方法是Main()本身,那么它返回给谁?它只是等待等待完成吗?这就是代码表现如此的原因(在打印“Completed”之前等待 5 秒)吗?

但这又引出了下一个问题:因为 DoWorkAsync() 本身在这里调用了另一个 wait 方法,当遇到 wait Task.Run() 行时,这显然要 5 秒后才能完成,DoWorkAsync() 不应该立即返回到调用方法 Main(),如果发生这种情况,Main() 不应该按照书作者的建议继续打印“Completed”吗?

顺便说一句,这本书是针对 C# 7 的,但我正在使用 C# 8 运行 VS 2019,如果这有什么区别的话。


我强烈建议您阅读 2012 年的这篇博文,当时await引入了关键字,但它解释了异步代码如何在控制台程序中工作:https://devblogs.microsoft.com/pfxteam/await-synchronizationcontext-and-console-apps/


然后作者指出

this 关键字 (await) 将始终修改返回 Task 对象的方法。当逻辑流达到awaittoken,调用线程在此方法中被挂起,直到调用完成。如果您要运行此版本的应用程序,您会发现“已完成”消息显示在“完成工作!”之前。信息。如果这是一个图形应用程序,用户可以继续使用 UI,同时DoWorkAsync()方法执行”。

作者说得不严谨。

我会改变这个:

当逻辑流达到awaittoken,调用线程在此方法中被挂起,直到调用完成

To this:

当逻辑流达到await令牌(即after DoWorkAsync返回一个Task对象),函数的本地状态保存在内存中的某个地方,并且正在运行的线程执行return回到异步调度程序(即线程池)。

我的观点是await不会导致线程“挂起”(也不会导致线程阻塞)。


下一句也是有问题的:

如果您要运行此版本的应用程序,您会发现“已完成”消息显示在“完成工作!”之前。信息

(我假设作者所说的“这个版本”指的是语法上相同但省略了的版本await关键词)。

所提出的主张是不正确的。被调用的方法DoWorkAsync仍然返回一个Task<String>这不可能是有意义地传递给Console.WriteLine: 返回的Task<String>必须是awaited first.


浏览有关 async/await 如何工作的各种在线文档和文章,我认为“await”会起作用,例如当遇到第一个“await”时,程序检查该方法是否已经完成,如果没有,它会立即“ return”到调用方法,然后在可等待任务完成后返回。

你的想法大体上是正确的。

但如果调用方法是Main()本身,那么它返回给谁呢?它只是等待等待完成吗?这就是代码表现如此的原因(在打印“Completed”之前等待 5 秒)吗?

它返回到由 CLR 维护的默认线程池。每个CLR程序都有一个线程池,这就是为什么即使是最微不足道的 .NET 程序进程也会出现在 Windows 任务管理器中,其线程数在 4 到 10 之间。但是,这些线程中的大多数将被挂起(但它们被挂起的事实与此无关)到使用async/await.


但这引出了下一个问题:因为DoWorkAsync()它本身在这里调用另一个awaited 方法,当await Task.Run()遇到了一行,这显然要 5 秒后才能完成,不应该DoWorkAsync()立即返回调用方法Main(),如果发生这种情况,不应该Main()按照书作者的建议继续打印“已完成”?

是和否:)

如果您查看已编译程序的原始 CIL (MSIL),这会有所帮助(await是一个纯粹的语法功能,不依赖于 .NET CLR 的任何实质性更改,这就是为什么async/await关键字是在 .NET Framework 4.5 中引入的,尽管 .NET Framework 4.5 运行在相同的 .NET 4.0 CLR 上,而 .NET 4.0 CLR 比它早了 3-4 年。

首先,我需要句法重新排列您的程序为此(此代码看起来不同,但它编译为与原始程序相同的 CIL (MSIL)):

static async Task Main(string[] args)
{
    Console.WriteLine(" Fun With Async ===>");     

    Task<String> messageTask = DoWorkAsync();       
    String message = await messageTask;

    Console.WriteLine( message );
    Console.WriteLine( "Completed" );

    Console.ReadLine();
}

static async Task<string> DoWorkAsync()
{
    Task<String> threadTask = Task.Run( BlockingJob );

    String value = await threadTask;

    return value;
}

static String BlockingJob()
{
    Thread.Sleep( 5000 );
    return "Done with work!";
}

发生的情况如下:

  1. CLR 加载您的程序集并定位Main入口点。

  2. CLR 还会用它从操作系统请求的线程填充默认线程池,它会立即挂起这些线程(如果操作系统本身没有挂起它们 - 我忘记了这些细节)。

  3. 然后,CLR 选择一个线程作为主线程,另一个线程作为 GC 线程(这还有更多细节,我认为它甚至可能使用主操作系统提供的 CLR 入口点线程 - 我不确定这些细节)。我们称之为Thread0.

  4. Thread0然后运行Console.WriteLine(" Fun With Async ===>");作为正常的方法调用。

  5. Thread0然后打电话DoWorkAsync() 也作为普通的方法调用.

  6. Thread0(里面DoWorkAsync)然后调用Task.Run,将委托(函数指针)传递给BlockingJob.

    • Remember that Task.Run is shorthand for "schedule (not immediately-run) this delegate on a thread in the thread-pool as a conceptual "job", and immediately return a Task<T> to represent the status of that job".
      • 例如,如果线程池耗尽或繁忙时Task.Run然后被称为BlockingJob在线程返回到池之前,或者如果您手动增加池的大小,则根本不会运行。
  7. Thread0然后立即给出Task<String>代表生命周期和完成BlockingJob。请注意,此时BlockingJob method 可以不可以尚未运行,因为这完全取决于您的调度程序。

  8. Thread0然后遇到第一个await for BlockingJob的职位Task<String>.

    • At this point actual CIL (MSIL) for DoWorkAsync contains an effective return statement which causes real execution to return to Main, where it then immediately returns to the thread-pool and lets the .NET async scheduler start worrying about scheduling.
      • 这就是事情变得复杂的地方:)
  9. So when Thread0返回线程池,BlockingJob可能会也可能不会被调用,具体取决于您的计算机设置和环境(例如,如果您的计算机只有 1 个 CPU 核心,情况会有所不同 - 但还有许多其他情况!)。

    • 这是完全有可能的 that Task.Run放在BlockingJob作业进入调度程序,然后才实际运行它Thread0本身返回到线程池,然后调度程序运行BlockingJob on Thread0并且整个程序只使用了单线程。
    • 但也有可能Task.Run会跑BlockingJob立即在另一个池线程上(这就是这个简单程序中可能的情况)。
  10. 现在,假设Thread0已屈服于池并且Task.Run在线程池中使用了不同的线程(Thread1) for BlockingJob, then Thread0将被暂停,因为没有其他计划的延续(从await or ContinueWith)也不是预定的线程池作业(来自Task.Run或手动使用ThreadPool.QueueUserWorkItem).

    • (请记住,挂起的线程与阻塞的线程不同! - 请参阅脚注 1)
    • So Thread1在跑BlockingJob它会休眠(阻塞)这 5 秒,因为Thread.Sleep块,这就是为什么你应该总是喜欢Task.Delay in async代码,因为它不会阻塞!)。
    • 那5秒之后Thread1然后解锁并返回"Done with work!"从那BlockingJob调用 - 并将该值返回给Task.Run的内部调度程序的调用站点和调度程序标记BlockingJob工作已完成"Done with work!"作为结果值(这由Task<String>.Result value).
    • Thread1然后返回到线程池。
    • 调度程序知道有一个await存在于那个之上Task<String> inside DoWorkAsync被使用过Thread0之前在步骤 8 中时Thread0回到了水池。
    • So because that Task<String> is now completed, it picks out another thread from the thread-pool (which may or may not be Thread0 - it could be Thread1 or another different thread Thread2 - again, it depends on your program, your computer, etc - but most importantly it depends on the synchronization-context and if you used ConfigureAwait(true) or ConfigureAwait(false)).
      • 在没有同步上下文的简单控制台程序中(即notWinForms、WPF 或 ASP.NET(但不是 ASP.NET Core)),那么调度程序将使用any池中的线程(即没有线程亲和力)。我们称之为Thread2.
  11. (我需要在这里离题解释一下,虽然你的async Task<String> DoWorkAsyncmethod 是 C# 源代码中的单个方法,但在内部DoWorkAsync每个方法被分成“子方法”await语句,每个“子方法”都可以直接进入)。

    • (它们不是“子方法”,但实际上整个方法被重写为隐藏的状态机struct捕获本地函数状态。参见脚注 2)。
  12. 所以现在调度程序告诉Thread2呼叫进入DoWorkAsync与紧随其后的逻辑相对应的“子方法”await。在这种情况下,它是String value = await threadTask; line.

    • 请记住,调度程序知道Task<String>.Result is "Done with work!",所以它设置String value到那个字符串。
  13. The DoWorkAsync子方法Thread2被调用的 then 也返回String value- 但不是Main,但是直接返回到调度程序 - 然后调度程序将该字符串值传递回Task<String>为了await messageTask in Main然后选择另一个线程(或同一个线程)进入Main的子方法,代表后面的代码await messageTask,然后该线程调用Console.WriteLine( message );以及正常方式的其余代码。


脚注

Footnote 1

请记住,挂起的线程与阻塞的线程不同:这是过于简单化的说法,但就本答案而言,“挂起的线程”具有空的调用堆栈,并且可以立即由调度程序投入工作做一些有用的事情,而“阻塞线程”有一个填充的调用堆栈,调度程序无法触及它或重新调整它的用途,除非它返回到线程池 - 请注意,线程可能会被“阻塞”,因为它很忙运行普通代码(例如while循环或自旋锁),因为它被同步原语(例如Semaphore.WaitOne,因为它正在睡觉Thread.Sleep,或者因为调试器指示操作系统冻结线程)。

Footnote 2

在我的回答中,我说过 C# 编译器实际上会围绕每个await语句进入“子方法”(实际上是一个状态机),这就是允许线程(any线程(无论其调用堆栈状态如何)来“恢复”其线程返回到线程池的方法。这是它的工作原理:

假设你有这个async method:

async Task<String> FoobarAsync()
{
    Task<Int32> task1 = GetInt32Async();
    Int32 value1 = await task1;

    Task<Double> task2 = GetDoubleAsync();
    Double value2 = await task2;

    String result = String.Format( "{0} {1}", value1, value2 );
    return result;
}

编译器将生成概念上与此 C# 相对应的 CIL (MSIL)(即,如果它是在没有async and await关键词)。

(This code omits lots of details like exception handling, the real values of state, it inlines AsyncTaskMethodBuilder, the capture of this, and so on - but those details aren't important right now)

Task<String> FoobarAsync()
{
    FoobarAsyncState state = new FoobarAsyncState();
    state.state = 1;
    state.task  = new Task<String>();
    state.MoveNext();

    return state.task;
}

struct FoobarAsyncState
{
    // Async state:
    public Int32        state;
    public Task<String> task;

    // Locals:
    Task<Int32> task1;
    Int32 value1
    Task<Double> task2;
    Double value2;
    String result;

    //
    
    public void MoveNext()
    {
        switch( this.state )
        {
        case 1:
            
            this.task1 = GetInt32Async();
            this.state = 2;
            
            // This call below is a method in the `AsyncTaskMethodBuilder` which essentially instructs the scheduler to call this `FoobarAsyncState.MoveNext()` when `this.task1` completes.
            // When `FoobarAsyncState.MoveNext()` is next called, the `case 2:` block will be executed because `this.state = 2` was assigned above.
            AwaitUnsafeOnCompleted( this.task1.GetAwaiter(), this );

            // Then immediately return to the caller (which will always be `FoobarAsync`).
            return;
            
        case 2:
            
            this.value1 = this.task1.Result; // This doesn't block because `this.task1` will be completed.
            this.task2 = GetDoubleAsync();
            this.state = 3;

            AwaitUnsafeOnCompleted( this.task2.GetAwaiter(), this );

            // Then immediately return to the caller, which is most likely the thread-pool scheduler.
            return;
            
        case 3:
            
            this.value2 = this.task2.Result; // This doesn't block because `this.task2` will be completed.

            this.result = String.Format( "{0} {1}", value1, value2 );
            
            // Set the .Result of this async method's Task<String>:
            this.task.TrySetResult( this.result );

            // `Task.TrySetResult` is an `internal` method that's actually called by `AsyncTaskMethodBuilder.SetResult`
            // ...and it also causes any continuations on `this.task` to be executed as well...
            
            // ...so this `return` statement below might not be called until a very long time after `TrySetResult` is called, depending on the contination chain for `this.task`!
            return;
        }
    }
}

注意FoobarAsyncState is a struct而不是一个class出于性能原因,我不会深入讨论。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

async Main() 中的等待行为令人困惑 的相关文章

  • 前向声明类型和“已声明为类类型的非类类型”

    我对以下代码有问题 template
  • 现代 C++ 编译器是否能够在某些情况下避免调用 const 函数两次?

    例如 如果我有以下代码 class SomeDataProcessor public bool calc const SomeData d1 const SomeData d2 const private Some non mutable
  • 当事件button.click发生时,如何获取按钮名称/标签?

    我以编程方式制作按钮并将它们添加到堆栈面板中 以便每次用户导航到页面时按钮都会发生变化 我正在尝试做这样的事情 当我单击创建的按钮时 它将获取按钮的标签并转到正确的页面 但是 我无法使用 RoutedEventHandler 访问按钮元素
  • 传递 constexpr 对象

    我决定给予新的C 14的定义constexpr旋转并充分利用它 我决定编写一个小的编译时字符串解析器 然而 我正在努力保持我的对象constexpr将其传递给函数时 考虑以下代码 include
  • 无法注册时间触发的后台任务

    对于 Windows 8 应用程序 在 C Xaml 中 我尝试注册后台任务 很难说 但我想我的后台任务已正确注册 但是当我单击调试位置工具栏上的后台任务名称时 我的应用程序停止工作 没有任何消息 我查看了事件查看器上的日志 得到 具有入口
  • Eigen 和 OpenMP:由于错误共享和线程开销而没有并行化

    系统规格 Intel Xeon E7 v3 处理器 4 插槽 16 核 插槽 2 线程 核心 Eigen 系列和 C 的使用 以下是代码片段的串行实现 Eigen VectorXd get Row const int j const int
  • 不同 C++ 文件中的相同类名

    如果两个 C 文件具有相同名称的类的不同定义 那么当它们被编译和链接时 即使没有警告也会抛出一些东西 例如 a cc class Student public std string foo return A void foo a Stude
  • 假装 .NET 字符串是值类型

    在 NET 中 字符串是不可变的 并且是引用类型变量 这通常会让新的 NET 开发人员感到惊讶 因为他们的行为可能会将它们误认为是值类型对象 然而 除了使用实践StringBuilder对于长连接 尤其是 在循环中 在实践中是否有任何理由需
  • 在 VS 中运行时如何查看 C# 控制台程序的输出?

    我刚刚编写了一个名为 helloworld 的聪明程序 它是一个 C NET 4 5 控制台应用程序 在扭曲的嵌套逻辑迷宫深处 使用了 Console WriteLine 当我在命令行运行它时 它会运行并且我会看到输出 我可以执行其他命令并
  • 如何使用 x64 运行 cl?

    我遇到了和这里同样的问题致命错误 C1034 windows h 未设置包含路径 https stackoverflow com questions 931652 fatal error c1034 windows h no include
  • 如何递归取消引用指针(C++03)?

    我正在尝试在 C 中递归地取消引用指针 如果传递一个对象 那就是not一个指针 这包括智能指针 我只想返回对象本身 如果可能的话通过引用返回 我有这个代码 template
  • 不可变类与结构

    以下是类与 C 中的结构的唯一区别 如果我错了 请纠正我 类变量是引用 而结构变量是值 因此在赋值和参数传递中复制结构的整个值 类变量是存储在堆栈上的指针 指向堆上的内存 而结构变量作为值存储在堆上 假设我有一个不可变的结构 该结构的字段一
  • 将二变量 std::function 转换为单变量 std::function

    我有一个函数 它获取两个值 x 和 y 并返回结果 std function lt double double double gt mult double x double y return x y 现在我想得到一个常量 y 的单变量函数
  • 将函数参数类型提取为参数包

    这是一个后续问题 解包 元组以调用匹配的函数指针 https stackoverflow com questions 7858817 unpacking a tuple to call a matching function pointer
  • 模板类中的无效数据类型生成编译时错误?

    我正在使用 C 创建一个字符串类 我希望该类仅接受数据类型 char 和 wchar t 并且我希望编译器在编译时使用 error 捕获任何无效数据类型 我不喜欢使用assert 我怎样才能做到这一点 您可以使用静态断言 促进提供一个 ht
  • Visual Studio 2015:v120 与 v140?

    仅供参考 Win10 x64 我今天开始尝试 Visual Studio 2015 在弄清楚如何运行 C C 部分后 我尝试加载一个大型个人项目 该项目使用非官方的glsdk http glsdk sourceforge net docs
  • 从共享网络文件夹运行的 .NET 应用程序的性能损失

    从共享网络文件夹运行 NET 4 0 应用程序是否有任何性能损失 我发现哪个应用程序启动速度较慢 但 在使用时没有注意到任何变慢 但不确定 当通过网络运行可执行文件时 Windows 不会在应用程序启动时通过网络传输整个应用程序 这样做是为
  • EntityFramework 6.0.0.0 读取数据,但不插入

    我创建了一个基于服务的数据库 folderName gt Add New Item gt Data gt Service based Database文件到 WPF 应用程序中 然后我用过Database First方法并创建了Person
  • 如何在 sql azure 上运行 aspnet_regsql? [复制]

    这个问题在这里已经有答案了 可能的重复 将 ASP NET 成员资格数据库迁移到 SQL Azure https stackoverflow com questions 10140774 migrating asp net membersh
  • MySqlConnectionStringBuilder - 使用证书连接

    我正在尝试连接到 Google Cloud Sql 这是一个 MySql 解决方案 我能够使用 MySql Workbench 进行连接 我如何使用 C 连接MySqlConnectionStringBuilder 我找不到提供这三个证书的

随机推荐

  • ARMv4/5/6 代码的哪些部分无法在 ARMv7 上运行?

    据我了解 ARMv7 处理器 例如 Cortex A9 大多向后兼容旧版 ARM 架构版本的代码 不过 我读过相关报道尝试在 Cortex A8 上运行 ARM9 代码时出现段错误 例如 ARMv4 5 6 ARM7TDMI ARM9 AR
  • 如何使用 JAX-RS 传输无尽的输入流

    我有无尽的InputStream一些数据 我想返回以响应GETHTTP 请求 我希望我的 Web API 客户端能够无休止地读取它 我怎样才能用 JAX RS 做到这一点 我正在尝试这个 GET Path stream Produces M
  • 在 Django 1.5 中导入 AUTH_USER_MODEL 的更好方法

    我正在尝试使可插入应用程序在 Django 1 5 下更具弹性 您现在拥有自定义的可定义用户模型 当向模型添加外键时 我可以这样做 user models ForeignKey settings AUTH USER MODEL 这节省了我在
  • 在Java中对二维字符串数组进行排序

    我知道这个问题以前可能有人问过 但我找不到合适的答案 假设我有这个数组 String theArray james 30 0 joyce 35 0 frank 3 0 zach 34 0 有没有办法按每个子元素的第二个元素对该数组进行降序排
  • 供开发人员使用的 LDAP 服务器

    我正在开发一个项目 需要 LDAP 验证 但是 我没有开发人员 qa ldap 服务器 Windows 是否存在用于测试 开发的小型 LDAP 服务器 我只想测试验证活动帐户并检测它是否被阻止 所以我不想安装整个域来执行此操作 没关系 我尝
  • Spring Security 6.x 已弃用 AccessDecisionVoter

    在 Spring Boot 2 7 x 中 我使用了RoleHierarchyVoter public RoleHierarchy roleHierarchy RoleHierarchyImpl roleHierarchy new Role
  • 带有变量数学运算符的 jQuery if 语句[重复]

    这个问题在这里已经有答案了 所以我正在寻找与这个问题类似的东西python if 语句与变量数学运算符但在 jQuery Javascript 中 本质上是这样的 var one 4 var two 6 var op if one op t
  • ES6 作为 AngularJS 或 Angular2 的 TypeScript 目标编译器选项

    我的编译器选项角js申请如下 我应该使用任何其他包来转译吗es6 to es5如果我再次将目标更改为es6 compilerOptions target es5 Change this to es6 module commonjs sour
  • docker compose 中的秘密

    我的环境是ubuntu 18 04 VPS 我无法获取基于文件的机密来与 Docker 容器中的 mariadb 一起使用 create docker compose yml version 3 7 services db image ma
  • 在 Android 文本范围上方绘制图像

    我正在创建一个复杂的文本视图 这意味着同一视图中存在不同的文本样式 某些文本需要在其上方有一个小图像 但文本应该仍然存在 而不仅仅是替换 因此简单的 ImageSpan 是行不通的 我无法使用 TextView 集合 因为我需要文本换行 或
  • 如何在“foreach”循环中获取当前索引/键[重复]

    这个问题在这里已经有答案了 在Java中 如何获取当前元素的索引 for Element song question song currentIndex lt
  • 如何从 Swift 调用 C?

    有没有办法从 Swift 调用 C 例程 许多 iOS Apple 库仅是 C 语言 我仍然希望能够调用它们 例如 我希望能够从 swift 调用 objc 运行时库 特别是 如何桥接 iOS C 标头 是的 您当然可以与 Apple 的
  • Actionmailer SMTP 服务器响应

    当通过actionmailer发送邮件时 actionmailer会从SMTP服务器获取响应 无论何时正常 或者何时错误 有没有办法在发送邮件后检索此回复 另外 当 SMTP 服务器没有抛出错误时 我们的 qmail 邮件服务器抛出一个处理
  • 使用 CSS 覆盖通过 JS 添加的内联样式

    一个js插件添加了一个让我有些头疼的样式 element style z index 100 important 所以我尝试过这个 html body div shell div shellContent div bottomPart di
  • 在标签库描述符中使用可变参数

    是否可以将 TLD 映射到以下功能 public static
  • User.IsInRole 返回 false

    我的 ASP NET 应用程序正在使用 Windows 身份验证 如果我运行以下代码 WindowsIdentity wi WindowsIdentity User Identity foreach IdentityReference r
  • 使用 LoDash 对 Json 数组进行排序

    我有一个 JSON 数组 其一般结构如下 var json key firstName value Bill key lastName value Mans key phone value 123 456 7890 实际上 会有更多的键 值
  • window.onbeforeunload 在 Chrome 中不起作用

    这是我使用的代码window onbeforeunload
  • Stream.reduce() 与 Stream.parallel.reduce()

    我真的很想知道之间的确切区别Stream reduce and Stream parallel reduce 为了清除所有内容 我创建了一个小程序 发现结果与相同的值不相等 public class Test public static v
  • async Main() 中的等待行为令人困惑

    我正在通过 Andrew Troelsen 的书 Pro C 7 With NET and NET Core 学习 C 在第 19 章 异步编程 中 作者使用了以下示例代码 static async Task Main string arg