文章目录
- 引言
- 一、Golang Goroutine
- 二、C++20 Coroutine
- 三、Coroutine 媲美Goroutine?
- 四、Coroutine 的影响力?
- 五、结语
摘要:C++20 Coroutine 和 Golang Goroutine 是现代编程语言解决并发和异步编程复杂性的重要原语。Goroutine 是 Go 语言的内置特性,易用、运行时调度,简化高并发应用的开发。C++20 Coroutine 是语言层面的低成本抽象,为 C++ 提供构建高性能异步库和框架的强大基石。
引言
基于操作系统线程的并发模型,虽然强大,但有着非常大的资源开销、复杂的同步机制、很难调试的竞态条件,“回调地狱”和上下文切换性能瓶颈。异步编程通过事件循环、回调函数、Future/Promise 等机制,不阻塞主线程的情况处理耗时操作,但代码可读性和维护性就不那么尽如人意了。
因此,轻量级并发原语(协程)逐渐崭露头角,成为优雅方案。用接近同步代码的直观方式编写异步逻辑,同时降低资源消耗和上下文切换的开销。其中,Golang 的 Goroutine 和 C++20 标准引入的 Coroutine 是各自语言生态最具代表性的轻量级并发模型,都承诺能最大简化并发编程,提升应用性能。
虽然目标相似,C++20 Coroutine 和 Golang Goroutine 的设计理念、底层实现、以及对语言生态的影响力上却大相径庭。Goroutine 是 Go 语言的核心特性,开箱即用、由运行时全面管理的并发解决方案;C++ Coroutine 只提供构建极致性能异步框架的底层工具。
这篇文章对这两种截然不同的轻量级并发原语进行深入的技术对比,剖析优势和局限,特别是 C++20 Coroutine 在技术能力上能否跟 Goroutine 匹敌,以及是否能像 Goroutine 那样,对 C++ 社区乃至整个软件行业带来同等量级的范式转变和深远影响力。
一、Golang Goroutine
Golang 是一门为并发而生的语言,并发是一等公民。Go 语言的 Goroutine 是实现并发编程的基础,简化高并发应用的开发。
Go 语言在设计之初就把并发视为核心竞争力:并发即服务。摒弃多线程编程的复杂性,而是利用 Tony Hoare 提出的通信顺序进程 (Communicating Sequential Processes, CSP)模型。这一模型的思想是“不要通过共享内存来通信,而要通过通信来共享内存”。Go 语言的 Goroutine 之间通过Channel(通道)进行数据交换和同步,而不是直接操作共享变量并依赖复杂的锁机制。从根本上避免传统并发编程常见的死锁、竞态条件等问题。
Goroutine 的实现机制:
- 跟操作系统线程不同,一个 Goroutine 初始时仅占用极小的栈空间(几 KB)。这个栈空间可以根据需要动态增长和收缩,避免不必要的内存浪费,所以 Go 程序可以轻松创建成千上万个 Goroutine。
- 运行时调度(M:N 模型):Goroutine 不直接映射到操作系统线程。Go 语言有一个内置的运行时调度器,负责把大量的 Goroutine (M) 多路复用到少量的操作系统线程 (N) 上。这个调度器是用户态的,可以在 Goroutine 执行 I/O 操作、等待 Channel 通信或进行函数调用等时机,自动暂停当前 Goroutine 并切换到另一个可运行的 Goroutine,不用操作系统内核的介入。这种 M:N 调度模型很大程度的减少上下文切换的开销,提高 CPU 利用率。
- 每个 Goroutine 都有自己的独立调用栈。Goroutine 可以在任何函数调用点暂停和恢复,局部变量和执行上下文都保存在自己的栈内。这种“栈式”的特性让 Goroutine 的编程模型非常直观,像编写同步代码一样编写并发逻辑,不用担心状态的保存和恢复。
- 通信机制:Channel。Channel 是 Goroutine 之间进行安全通信和同步的主要方式。是一种类型安全的管道,可以一个 Goroutine 向其发送数据,另一个 Goroutine 从中接收数据。Channel 可以是带缓冲的或不带缓冲的,提供天然的同步点,确保数据在并发环境有序和正确传递。Go 语言还提供
select语句,同时监听多个 Channel 的操作,实现复杂的并发控制逻辑。
Goroutine 的优势:
- 创建 Goroutine 的语法极其简单,只要在函数调用前加上
go关键字。这种直观的语法大大降低并发编程的门槛。 - 不用关心线程池管理、锁机制、死锁预防等复杂问题,只要关注业务逻辑和数据流。Goroutine 结合 Channel 的模型让并发代码的编写和理解变得更加容易。
- 内置调度器:Go 运行时自动管理 Goroutine 的生命周期、调度和上下文切换,不用手动编写复杂的调度逻辑。所以, Go 程序能充分利用多核处理器,同时保持良好的可伸缩性。
- Go 语言的标准库和庞大的第三方库都对 Goroutine 和 Channel 有原生、良好的支持,构建复杂的并发应用轻而易举。
Goroutine 的局限:
- 运行时依赖:Goroutine 的性能和行为高度依赖 Go 运行时。对运行时开销有极致要求,或者要完全控制底层内存布局和调度策略的场景,Go 语言的抽象层级就不够低。
- 一定程度的抽象:虽然 Goroutine 提供高效的并发,但毕竟是 Go 语言运行时提供的一种抽象。不能像 C++ 那样,直接操作寄存器、手动管理协程帧,实现极致的底层控制和“零开销”抽象。
Golang 的 Goroutine 擅长场景:
- 网络服务和微服务:构建高并发的 Web 服务器、API 网关和分布式服务。
- 高并发后端应用:处理大量并发请求。
- 数据处理管道:用 Channel 轻松构建数据流处理管道,实现并行数据处理。
- 批处理任务:把大型任务分解为多个 Goroutine 并行执行,加速处理速度。
二、C++20 Coroutine
C++20 标准引入的 Coroutine (协程) 和 Go 语言把并发作为一等公民的设计理念不同,C++ Coroutine 秉承 C++ 一贯的“零成本抽象”,提供的是一套低层级的语言机制,而不是一个完整的并发解决方案。目标是成为构建高性能异步库和框架的基础。
C++ Coroutine 的设计目标是提供一种机制,能编写看起来像同步代码的异步操作,同时保持 C++ 固有的性能和控制力。
- Coroutine 机制本身不引入额外的运行时开销,性能跟手动编写的状态机相当。编译器负责把协程转换为高效的代码。
- C++ Coroutine 不是一个“开箱即用”的并发模型,不包含内置的调度器、I/O 库或通信机制。相反,只提供一套通用的语言特性,在此基础上构建各种复杂的异步抽象。
- Coroutine 的主要受众是库和框架的作者,可以用这一机制来创建更高效、更易用的异步 API。
C++ Coroutine 的实现机制跟 Goroutine 有很大的不一样:
- 编译器转换(状态机):编译器遇到一个标记为协程的函数时(也就是包含
co_await,co_yield,co_return关键字的函数),会把其转换为一个状态机。这个状态机负责在协程暂停和恢复时保存和恢复其执行上下文。 - 无栈是 C++ Coroutine 跟 Goroutine 最明显的区别之一。C++ Coroutine 是“无栈”的,协程暂停时,其调用栈会被完全展开,局部变量和状态不会保存在传统的调用栈上。相反,协程的状态(包括局部变量、参数和返回地址)会被编译器打包到一个称为“协程帧”(Coroutine Frame)的结构,这个结构是在堆上分配的。这种设计避免传统线程上下文切换时栈的复制或切换开销,让协程切换极其轻量。
- 无内置调度器:C++ 标准库只定义协程的语法和底层机制,没有提供任何形式的调度器。协程的暂停和恢复由
co_await表达式决定,但何时以及如何在哪个线程上恢复协程的执行,完全取决用户或库提供的执行器。C++ Coroutine 可以跟任何事件循环、线程池或 I/O 框架直接集成。
核心关键字:co_await,co_yield,co_return:
co_await:暂停当前协程的执行,等待一个“可等待对象”完成,并在可等待对象完成后恢复执行。co_yield:生成一个值并暂停协程,使其成为一个生成器。co_return:从协程返回一个值或表示协程的完成。
C++ Coroutine 的灵活性和可扩展性得益于其底层的Promise 类型和 Awaitable 概念。每个协程都跟一个 Promise 类型关联,该类型定义协程的返回值类型、异常处理方式以及协程的生命周期管理。Awaitable 对象定义await_ready、await_suspend和await_resume三个方法,控制协程何时暂停、如何暂停以及如何恢复。所以,任何人都可以高度定制协程的行为。
C++ Coroutine 的优势:
- 因为无栈特性和编译器优化的状态机,C++ Coroutine 的上下文切换开销理论上可以接近零,远低于操作系统线程切换,甚至低于 Goroutine 的运行时调度开销。让 C++ Coroutine 在对延迟和吞吐量有极高要求的场景下表现卓越。
- 可以完全控制协程的生命周期、内存分配(协程帧的分配位置)、以及调度策略。这种底层控制能力是 C++ 语言的精髓,让使得异步代码能以线性的、同步风格的方式书写,彻底告别异步编程常见的“回调地狱”问题。
- 结合 Promise 类型和 Awaitable 概念,C++ Coroutine 能构建出非常复杂、高效的异步流程、事件驱动架构和状态机。
C++ Coroutine 的缺点:
- 学习曲线陡峭:相比 Goroutine 的简洁语法,C++ Coroutine 的概念(Promise 类型、Awaitable、协程帧、执行器集成等)相对复杂,要开发者对 C++ 语言特性和异步编程有更深入的理解,学习门槛较高。
- C++ 标准库不提供开箱即用的调度器和异步 I/O 库。要依赖第三方库或自行实现来管理协程的调度和集成异步 I/O 操作。
- 虽然很多优秀的库和框架正在积极采用 C++ Coroutine,但生态系统相比 Goroutine 而言还处在发展初期,缺乏统一的、被广泛接受的解决方案,有一定程度的碎片化。
C++ Coroutine 的应用场景:
- 高性能网络库:构建高效的异步网络服务器、客户端和代理。
- 游戏引擎:实现复杂的动画序列、AI 行为和游戏逻辑,提高帧率和响应性。
- 实时系统:对时间敏感的系统用其低延迟特性处理并发事件。
- 异步 I/O 框架:作为底层构建块,为文件操作、数据库访问提供高效的异步接口。
- 事件驱动架构:简化复杂事件处理流程的编写。
三、Coroutine 媲美Goroutine?
不同层级的抽象:构建块 vs. 完整解决方案。这是两者最根本的区别:
- Goroutine:完整的并发解决方案。Goroutine 是 Go 语言运行时的一部分,提供一套从创建、调度到通信的完整、开箱即用的并发模型。只要用
go关键字,Go 运行时就会自动管理 Goroutine 的生命周期、调度和跟操作系统线程的映射。是一个高层级的抽象,把并发的复杂性封装在语言和运行时内部。 - C++ Coroutine:低层级的语言构建块。C++ Coroutine 不是一个完整的并发模型,而是 C++ 语言提供的一组底层机制(
co_await,co_yield,co_return、Promise 类型和 Awaitable 概念)。不包含内置的调度器或 I/O 库。C++ Coroutine 的目标是作为构建高性能异步库和框架的基石,要在其之上构建自己的调度器、任务抽象和异步 I/O 接口。是一个低层级的抽象,把控制权最大化交给开发者。
所以,如果是提供一个同样简单易用、开箱即用的并发模型,那么 C++ Coroutine 本身无法直接跟 Goroutine 媲美,因为要额外的库支持。但如果指的是作为构建高性能异步系统的底层能力,那么 C++ Coroutine 有很大的潜力。
性能方面,各有侧重。
C++ Coroutine 的优势:极致性能和精细控制。
- C++ Coroutine 的无栈特性和编译器生成的状态机,上下文切换开销理论上可以做到极低,接近函数调用,远低操作系统线程切换。
- 因为没有内置调度器,C++ Coroutine 避免 Go 运行时调度器带来的固定开销。甚至可以完全避免调度器,直接在事件循环恢复协程。
- 协程帧的分配可以由用户控制,甚至可以实现无堆分配的协程。
- 适用场景:对延迟、吞吐量和资源利用率有极致要求的场景,如高频交易系统、游戏引擎、实时嵌入式系统、高性能网络库等,C++ Coroutine 能有非常大的优化空间。
Goroutine 的优势:整体效率和易用性。
- 高效的 M:N 调度:Go 运行时调度器在大多数通用场景下表现都非常出色,能把大量 Goroutine 映射到少量 OS 线程,充分利用多核 CPU。
- 内置 I/O 多路复用:Go 运行时集成了网络 I/O 的多路复用,让 Goroutine 在等待 I/O 时能自动切换,不用手动管理。
- 绝大多数高并发应用而言,Goroutine 提供的性能已经绰绰有余,并且在开发效率的加持下,综合效益更高。
从绝对的理论性能和底层控制力来看,C++ Coroutine 有超越 Goroutine 。但在大多数实际应用,Goroutine 提供的“足够好”的性能和极高的开发效率相结合,整体效率上更有竞争力。
开发体验和效率方面,两者差异就非常明显了:
- Goroutine 学习曲线平缓、开箱即用、快速迭代。
- C++ Coroutine 学习曲线陡峭。概念复杂、要手动集成、初期开发效率低。
但是,只要 C++ Coroutine 的上层库(std::expected和std::generator)成熟,使用体验就变得非常简洁。届时,C++ Coroutine 在使用层面的易用性就能跟 Goroutine 媲美,甚至更胜一筹。
生态系统和标准化:
- Goroutine:统一、成熟的生态。Goroutine 是 Go 语言的核心特性,都用相同的并发模型。而且 Go 标准库的各个模块都原生支持 Goroutine 和 Channel,形成一个统一、强大的生态系统。围绕 Goroutine 和 Channel 形成一套清晰的并发编程范式,社区资源丰富。
- C++ Coroutine:发展中、碎片化。缺乏标准化调度器/I/O 库,生态碎片化风险。很多优秀的 C++ 库正在积极整合 C++ Coroutine,未来会有在标准上出现。
C++ Coroutine 在技术能力和潜力上足以媲美甚至超越 Goroutine,尤其是要极致性能和底层控制的领域。
但是,在易用性和开箱即用性上,Goroutine 还是有主导优势。提供完整的、容易学习和使用的并发模型,能快速构建高并发应用。
- Goroutine适合追求开发效率、快速迭代、以及在通用高并发场景下获得“足够好”性能的开发者。
- C++ Coroutine适合追求极致性能、精细控制、以及作为库和框架作者构建高性能异步基础设施的开发者。
四、Coroutine 的影响力?
Goroutine 的影响力不只是技术上的先进,更在对 Go 语言生态、开发者社区乃至整个行业产生的深远影响:定义 Go 语言的并发范式,降低并发编程的门槛,引发广泛的范式转变。以此为标准,审视 C++ Coroutine 的潜力。
Goroutine 的影响力:
- Goroutine 几乎是 Go 语言的代名词。和 Channel 结合,共同构成了 Go 语言独有的、简洁而强大的并发模型。任何学习 Go 语言的开发者,都会首先接触并掌握 Goroutine。
go func()的简单语法,结合 Go 运行时自动调度和 Channel 的安全通信机制,大量非专业的并发编程开发者也能编写出高效、正确的并发代码。- Goroutine 构建高并发、高伸缩性的网络服务非常简单,这跟微服务和云原生架构的需求高度契合,从而在很大程度上推动这些技术栈的普及。
- Goroutine 的成功,让更多语言和开发者看到轻量级协程的巨大潜力,加速其他语言对类似机制的采纳和发展。
虽然 C++ Coroutine 在技术有强大的潜力,但要带来 Goroutine 那样的广泛影响力,还不够:
- C++ 是构建块,不是完整解决方案。C++ 语言只是提供低层级的、零成本的抽象,在此基础上构建自己的解决方案。Coroutine 它本身不是一个完整的、开箱即用的并发模型。不提供标准化的调度器、异步 I/O 库或通信机制。要达到 Goroutine 那样的易用性,C++ 要在 Coroutine 之上构建一个或多个事实上的标准库/框架,而这要时间,且不保证能形成统一。
- 相比
go func()的简洁直观,C++ Coroutine 涉及的概念(Promise 类型、Awaitable、协程帧、执行器集成、生命周期管理等)更为复杂和底层。掌握这些概念并要投入相当大的学习成本。 - 生态系统碎片化。标准库没有提供统一的调度器和异步 I/O 库,C++ Coroutine 的使用依赖各种第三方库。生态系统碎片化,跨项目和跨库的协作不如 Go 那样统一和简单。
- C++ 已经有
std::thread、std::future、std::async以及各种锁和原子操作等成熟的并发工具。Coroutines 是对现有工具的补充和增强,尤其是在异步 I/O 方面,而不是一个颠覆性的替代品。它更多地是解决特定领域的痛点,而不是改变整个 C++ 的并发编程范式。
虽然很难复制 Goroutine 的影响力模式,C++ Coroutine 还是对 C++ 社区和相关行业产生深远、独特的影响:
- 提升 C++ 在异步编程领域的竞争力:Coroutine 改善 C++ 编写高性能、高可读性异步代码方面的能力。C++ 在网络服务、游戏引擎、实时系统、高性能计算等对性能和响应性有极致要求的领域,能够更好应对现代异步编程的挑战,保持领先地位。
- Coroutine 能把复杂的异步流程以同步代码的风格表达,彻底告别 C++ 长期存在的“回调地狱”问题。
- Coroutine 为库和框架的作者提供强大的底层工具,构建更高效、更易用的异步抽象。未来会看到更多基于 Coroutine 的高性能网络库、任务调度框架和异步 I/O 库涌现,这些库将间接提升整个 C++ 生态的异步编程水平。
- 推动 C++ 现代化进程。
结论:影响力不同,但同样重要。
C++20 Coroutine 不太可能像 Golang 的 Goroutine 那样,用一种统一、简单的方式,彻底改变 C++ 语言的并发编程范式,并带来同等规模的、对整个行业产生广泛影响力的范式转变。它的影响力更多体现在提升 C++ 在特定高性能异步领域的竞争力,改善现有复杂异步代码的维护性上。
五、结语
Golang 的 Goroutine开箱即用的特性、极简的语法和内置的运行时调度器,成功把并发编程的门槛降至前所未有的低点。
C++20 Coroutine提供的是一套低层级的、极致灵活的语言构建块。不包含内置的调度器或 I/O 库,而是把这些职责交由库作者和开发者自行实现。
关于 C++ Coroutine 能否跟 Goroutine 媲美,以及能否带来同等影响力的问题,结论是:
- 在技术能力和潜力上,C++ Coroutine 足以媲美甚至超越 Goroutine。特别是极致性能和底层控制的领域,C++ 在异步编程领域有跟 Go 语言一较高下的底层能力。
- 但是,C++ Coroutine 不太可能带来 Goroutine 那样广泛的、对整个行业产生范式转变的影响力。
随着 C++ Coroutine 生态系统的逐渐成熟,特别是标准化或事实上的标准执行器和异步 I/O 库出现,C++ Coroutine 的易用性会大大提升。届时,C++ 开发者能以更简洁、高效的方式利用这一强大特性。