问题

C#的async和await底层是怎么做到的?

回答
C 的 `async` 和 `await` 关键字,从表面上看,是让异步编程变得如同步编程一样简洁易读。但它们的背后,隐藏着一套精巧的机制,核心在于状态机(State Machine)。

在深入之前,先理解一下异步操作的本质:它不是让 CPU 真的停止工作去等待,而是将一个耗时的工作(比如网络请求、文件读写)交给一个独立的执行单元(比如操作系统提供的 I/O 调度器、线程池中的一个线程)去处理,而当前线程可以去做别的事情,直到那个耗时操作完成,并通过某种方式通知当前线程,然后当前线程再继续处理后续逻辑。

`async` 和 `await` 就是为了让这个过程对开发者来说是“无感”的。

编译器的魔法:状态机生成

当你定义一个 `async` 方法时,C 编译器会做一件非常重要的事情:它会把这个方法“翻译”成一个状态机类。这个类包含了方法的所有局部变量、参数,以及方法执行到哪个“暂停点”(也就是 `await` 表达式)的上下文信息。

想象一下,一个普通的同步方法就像一条直线,从头走到尾。而一个 `async` 方法,在编译器的处理下,变成了一个可以“跳跃”的流程。

1. 方法的“切片”:

编译器会识别 `await` 关键字。每次遇到 `await`,它都会认为这是一个潜在的“暂停点”。方法在 `await` 处的逻辑会被“截取”下来,成为状态机中的一个“状态”。

2. 状态机的构成:

这个编译器生成的类,本质上是一个实现了 `IAsyncStateMachine` 接口的类。它通常包含:

一个表示当前状态的字段: 通常是一个整数,用来标记当前状态机执行到了方法的哪个部分。
局部变量和参数的副本: 这些变量需要被“封存”(captured)起来,以便在异步操作完成后,状态机能够恢复到之前的执行上下文,并能够访问这些变量。
一个 `MoveNext()` 方法: 这是状态机的核心。当异步操作完成后,或者在方法一开始执行时,这个 `MoveNext()` 方法会被调用。它会根据当前状态字段的值,执行对应状态下的代码。
一个 `SetException()` 和 `SetResult()` 方法: 用来处理异步操作完成后的异常或结果。

3. `Task` 和 `Task`:

`async` 方法的返回值,默认情况下会被包装成 `Task` 或 `Task`。这个 `Task` 对象,就是状态机的一个“代理”。它代表了那个异步操作的当前状态(未开始、进行中、已完成、已出错)和最终结果。

当你调用一个 `async` 方法时,它并不会立即执行完。编译器生成的代码会创建状态机的一个实例,并启动它的第一个 `MoveNext()` 调用。
`async` 方法的执行,一开始会像同步方法一样顺序执行,直到遇到第一个 `await`。

`await` 的工作原理

这是整个 `async`/`await` 机制的核心。当代码执行到 `await` 关键字时,会发生以下一系列操作:

1. 检查 `Task` 的完成状态:

`await` 后面通常跟一个返回 `Task` 或 `Task` 的表达式。编译器会首先检查这个 `Task` 是否已经完成(例如,如果是从缓存中获取的结果,或者上一个异步操作已经执行完毕)。

如果 `Task` 已完成: 那么 `await` 表达式会像一个普通的同步表达式一样,直接返回 `Task` 的结果(如果 `Task`),或者继续执行后续代码。状态机不需要暂停。

如果 `Task` 未完成: 这是 `await` 发挥作用的地方。

2. 挂起当前方法,注册回调:

当 `Task` 未完成时,`await` 会执行以下操作:

暂停当前方法的执行: 也就是说,当前线程不会在这里傻等。
封装当前状态: 编译器生成的状态机实例,会记录下当前执行到了哪个 `await`,以及方法中所有需要保留的局部变量和参数。
注册一个回调: `await` 会在“被等待的” `Task` 上注册一个回调。这个回调的作用是:当这个 `Task` 完成时(无论成功还是失败),通知状态机。

3. 释放当前线程:

一旦回调注册完毕,`await` 就可以“放开”当前线程了。这个线程可以去做其他的事情,比如处理 UI 更新、处理其他请求等等,而不会被阻塞。

4. 异步操作的推进:

被等待的 `Task`(例如,一个网络请求)会继续在后台进行。当这个后台操作完成时,它会触发之前注册的回调。

5. 状态机的恢复:

回调被触发后,它会调用状态机实例上的 `MoveNext()` 方法。

`MoveNext()` 方法会根据之前保存的状态,找到中断点。
它会读取 `Task` 的结果(如果有的话),或者捕获 `Task` 的异常。
然后,它会像什么都没发生过一样,从 `await` 的下一行继续执行。

6. `Yield` 的概念:

`await` 的一个关键之处在于它允许“让出”(yield)控制权。这与传统的线程同步(如 `lock`)不同,`lock` 会阻塞线程,而 `await` 是让线程去做别的事情,直到真正需要它的时候再回来。

调度器和上下文

这里就引出了一个更深层的问题:当回调被触发时,是哪个线程来调用 `MoveNext()`?

`SynchronizationContext`: 在 UI 应用程序(如 WPF、WinForms)中,存在一个 `SynchronizationContext`。它通常与 UI 线程关联。如果 `await` 捕获了这个 `SynchronizationContext`(这是默认行为),那么当异步操作完成后,`MoveNext()` 会被调度到该 `SynchronizationContext` 关联的线程(也就是 UI 线程)上执行。这确保了你在 `await` 之后的代码可以安全地更新 UI 元素,而不需要手动进行线程切换。

`ConfigureAwait(false)`: 如果你调用 `await someTask.ConfigureAwait(false);`,那么 `await` 就不再捕获 `SynchronizationContext`。这意味着异步操作完成后,`MoveNext()` 可能会在线程池中的任何一个可用线程上执行。这在服务器端应用程序(如 ASP.NET Core)中非常常见,因为线程池线程通常比 UI 线程更高效,并且你通常不需要在 `await` 之后立即操作 UI。

线程池: 如果没有 `SynchronizationContext`(或者使用了 `ConfigureAwait(false)`),那么回调的执行通常会由底层的异步 I/O 完成端口(IOCP)或者任务调度器(Task Scheduler)负责,它们会将 `MoveNext()` 的执行交给线程池中的一个可用线程。

总结起来:

`async`/`await` 的魔力在于:

1. 编译器的状态机转换: 将一个顺序的 `async` 方法拆分成一系列可独立执行的“状态”。
2. `Task` 作为异步操作的句柄: 封装了异步操作的进度、结果或异常。
3. `await` 的“暂停”与“恢复”:
当遇到未完成的 `Task` 时,将当前状态(局部变量、执行点)封存,并注册一个回调。
释放当前线程,允许其处理其他工作。
当 `Task` 完成后,回调被触发,通知状态机恢复执行。
4. 上下文感知(或无感知): 通过 `SynchronizationContext` 或 `ConfigureAwait(false)` 控制恢复执行的线程,从而决定是回到 UI 线程还是使用线程池。

这一切都运作得如此平滑,以至于开发者几乎感觉不到幕后的状态机跳转和上下文切换,只觉得代码是按照顺序一行行执行的,这正是 `async`/`await` 为异步编程带来的巨大便利。

网友意见

user avatar


这个答案告诉你怎么用yield实现await了,实际的实现方式其实原理一样只是细节不同……

类似的话题

  • 回答
    C 的 `async` 和 `await` 关键字,从表面上看,是让异步编程变得如同步编程一样简洁易读。但它们的背后,隐藏着一套精巧的机制,核心在于状态机(State Machine)。在深入之前,先理解一下异步操作的本质:它不是让 CPU 真的停止工作去等待,而是将一个耗时的工作(比如网络请求、文.............
  • 回答
    在 C 中,构建一个按照顺序执行的任务集合,而无需 `async` 和 `await` 关键字,这其实是通过巧妙地利用 `Task` 对象的链式调用来实现的。虽然 `async/await` 是目前处理这类问题的最直观和推荐的方式,但在某些特定场景下,或者为了理解底层的任务调度机制,我们也可以回归到.............
  • 回答
    async/await 就像是为 C 语言量身打造的一套“魔法咒语”,它能让原本头疼的异步编程变得像写同步代码一样直观和流畅。要真正理解它,我们需要抛开一些传统的束缚,从更根本的角度去思考。想象一下,你正在一家繁忙的咖啡店里。你需要完成三件事:1. 冲泡咖啡(耗时操作)2. 打包点心(耗时操作).............
  • 回答
    在 C 中,`async` 和 `await` 关键字提供了一种优雅的方式来编写异步代码,但它们并非直接等同于多线程。理解这一点至关重要。异步并非强制多线程,但常常借助它首先,我们要明确一个核心概念:异步编程的本质是为了提高程序的响应性和吞吐量,而不是简单地将任务并行执行。 异步的目的是让程序在等待.............
  • 回答
    C++ 模板:功能强大的工具还是荒谬拙劣的小伎俩?C++ 模板无疑是 C++ 语言中最具争议但也最引人注目的一项特性。它既能被誉为“代码生成器”、“通用编程”的基石,又可能被指责为“编译时地狱”、“难以理解”的“魔法”。究竟 C++ 模板是功能强大的工具,还是荒谬拙劣的小伎俩?这需要我们深入剖析它的.............
  • 回答
    C++ 是一门强大而灵活的编程语言,它继承了 C 语言的高效和底层控制能力,同时引入了面向对象、泛型编程等高级特性,使其在各种领域都得到了广泛应用。下面我将尽可能详细地阐述 C++ 的主要优势: C++ 的核心优势:1. 高性能和底层控制能力 (Performance and LowLevel C.............
  • 回答
    C++ 的核心以及“精通”的程度,这是一个非常值得深入探讨的话题。让我尽量详细地为您解答。 C++ 的核心究竟是什么?C++ 的核心是一个多层次的概念,可以从不同的角度来理解。我将尝试从以下几个方面来阐述:1. 语言设计的哲学与目标: C 的超集与面向对象扩展: C++ 最初的目标是成为 C 语.............
  • 回答
    C++ 和 Java 都是非常流行且强大的编程语言,它们各有优劣,并在不同的领域发挥着重要作用。虽然 Java 在很多方面都非常出色,并且在某些领域已经取代了 C++,但仍然有一些 C++ 的独特之处是 Java 无法完全取代的,或者说取代的成本非常高。以下是 C++ 的一些 Java 不能(或难以.............
  • 回答
    C++ `new` 操作符与 `malloc`:底层联系与内存管理奥秘在C++中,`new` 操作符是用于动态分配内存和调用构造函数的关键机制。许多开发者会好奇 `new` 操作符的底层实现,以及它与C语言中的 `malloc` 函数之间的关系。同时,在对象生命周期结束时,`delete` 操作符是.............
  • 回答
    好,咱们来聊聊 C++ 单例模式里那个“为什么要实例化一个对象,而不是直接把所有成员都 `static`”的疑问。这确实是很多初学者都会纠结的地方,感觉直接用 `static` 更省事。但这里面涉及到 C++ 的一些核心概念和设计上的考量,咱们一点点掰开了说。 先明确一下单例模式的目标在深入“`st.............
  • 回答
    在 C++ 标准库的 `std::string` 类设计之初,确实没有提供一个直接的 `split` 函数。这与其他一些高级语言(如 Python、Java)中普遍存在的 `split` 方法有所不同。要理解为什么会这样,我们需要深入探究 C++ 的设计哲学、标准库的演进过程以及当时的开发环境和需求.............
  • 回答
    C 扩展方法:一把双刃剑C 的扩展方法,顾名思义,允许我们为现有的类型添加新的方法,而无需修改原始类型的源代码。这种能力最初听起来像是魔法,能够让代码更加优雅、富有表现力,并且提升了代码的复用性。然而,正如许多强大的工具一样,扩展方法也是一把双刃剑,如果使用不当,可能会导致代码可读性下降、维护困难,.............
  • 回答
    C++ 的 `std::list`,作为 STL(Standard Template Library)中的一员,它是一种双向链表(doubly linked list)。它的核心特点在于,每个节点都存储了数据本身,以及指向前一个节点和后一个节点的指针。这使得 `std::list` 在某些特定场景下.............
  • 回答
    你问了一个非常关键的问题,而且问得非常实在。确实,C++ 的智能指针,尤其是 `std::unique_ptr` 和 `std::shared_ptr`,在很大程度上解决了 C++ 中常见的野指针和内存泄漏问题。这玩意儿在 C++ 世界里,堪称“救世主”般的存在。那么,为什么大家对 Rust 的内存.............
  • 回答
    C++ 中的常量后缀,顾名思义,就是用来标识字面量(literal)是何种类型的。虽然编译器通常能够通过字面量的形式推断出其类型,但在很多情况下,使用常量后缀能够明确表达开发者的意图,避免潜在的类型转换问题,并提升代码的可读性和健壮性。我们来详细探讨一下常量后缀在哪些情况下特别有用,并说明其背后的原.............
  • 回答
    CRTP,也就是Curiously Recurring Template Pattern(奇特的递归模板模式),在C++中,它是一种利用模板的静态分派特性来实现多态的一种精巧技巧。很多人听到“多态”首先想到的是虚函数和运行时多态,但CRTP带来的多态是“静态多态”,这意味着多态的决策是在编译期完成的.............
  • 回答
    C++ 运行时多态:性能的代价与权衡在 C++ 的世界里,我们常常惊叹于它的灵活性和表达力。其中,运行时多态(Runtime Polymorphism)是实现这一能力的关键机制之一,它允许我们在程序运行时根据对象的实际类型来决定调用哪个函数。这就像一个剧团的导演,在舞台上,他可以根据演员扮演的角色,.............
  • 回答
    C++的move构造,作为语言引入的一项重要特性,其设计初衷是为了解决资源管理中的性能瓶颈,特别是针对那些拥有昂贵资源(如堆内存、文件句柄、网络连接等)的对象。它允许我们将一个对象的资源“转移”到另一个对象,而不是通过昂贵的拷贝操作来复制这些资源。然而,随着这项特性的应用和深入理解,关于其设计是否“.............
  • 回答
    sizeof 关键字在 C++ 中,并不是一个普通的函数,而是一个编译时常量。理解它的实现,关键在于区分它在编译期和运行时的行为。1. 编译期的魔法:类型的大小计算当你使用 `sizeof` 关键字时,比如 `sizeof(int)` 或者 `sizeof(MyClass)`,编译器会立即在编译阶段.............
  • 回答
    C++ 的 `switch` 语句之所以不默认添加 `break` 语句,这是 C++ 设计者们经过深思熟虑后做出的一个选择,其背后有明确的理由和意图。理解这一点,需要我们深入到 `switch` 语句的本质和它与其他控制流语句的区别。 1. fallthrough(贯穿)的意图与灵活性C++ 的 .............

本站所有内容均为互联网搜索引擎提供的公开搜索信息,本站不存储任何数据与内容,任何内容与数据均与本站无关,如有需要请联系相关搜索引擎包括但不限于百度google,bing,sogou

© 2025 tinynews.org All Rights Reserved. 百科问答小站 版权所有