技术要求
为了运行代码示例,你必须准备以下内容:
-
一个能够编译和执行C++20的基于Linux的系统(例如,Linux Mint 21)
-
GCC12.2编译器 - https://gcc.gnu.org/git/gcc.git gcc-source:使用-fcoroutines、-std=c++2a、-lpthread和-lrt标志
-
对于某些示例,你也可以选择使用https://godbolt.org/。
介绍协程
一个进程就是一个程序的运行实例。它有自己的地址空间,除了通过共享内存,不与其他进程共享。线程存在于进程中,它们不能脱离进程存在,尽管在Linux中,进程和线程都被视为任务。它们以相同的方式被调度,并且在内核级别有相同的控制结构。尽管如此,线程被认为是轻量级的,因为程序的初始负载的较大开销由父进程承担。
但这并不是完整的情况。还有纤程和协程。如果说进程和线程是真正的并发并且在共享资源上并行工作,纤程就像线程,但不符合并发。虽然线程通常依赖于任务调度器的抢占式时间分片,纤程使用协作式多任务处理。也就是说,它们在执行过程中自己让出控制权,以运行另一个纤程。它们也被称为有栈协程。与此同时,C++中的协程被称为无栈协程,不由操作系统管理。换句话说,有栈协程可以在嵌套的栈帧中被挂起,而无栈协程只能通过顶级例程嵌套。
协程技术相当古老。C++最近才引入它,它对于网络编程、I/O操作、事件管理等非常有用。协程也被认为是具有暂停能力的执行。尽管如此,它们以协作方式提供多任务处理,并不并行工作。这意味着任务不能同时执行。同时,它们是实时友好的,允许在协程之间快速切换上下文,不需要系统调用。事实上,它们对硬实时操作系统友好,因为执行顺序和调度由系统程序员控制,正如你稍后将在本章中看到的。C++中的协程非常适用于实现任务图和状态机等。
你们可能想知道协程和标准单线程函数式编程之间的区别。嗯,后者被认为是同步方法,而前者是具有同步可读性的异步方法。但协程真正关注的是减少不必要的等待,并在准备所需资源或调用时做一些有用的事情。以下简图虽然简单,但提醒我们同步和异步执行之间的相应区别。
图1同步与异步应用程序执行
普通的单线程执行在某些方面也是有限的。首先,程序内部无法追踪调用、挂起或恢复函数,或者至少不能通过引用追踪。换句话说,控制流在后台发生且是隐式的。此外,控制流有一个严格的方向 - 函数要么返回到其调用者,要么继续向内调用另一个函数。每个函数调用在栈上创建一个新记录,并立即发生,一旦调用,方法不能被延迟。一旦该函数返回,其在栈上的部分就被清除,无法恢复。换言之,激活是无法追踪的。
另一方面,协程拥有自己的生命周期。协程是一个对象,可以明确地被引用。如果协程应该比其调用者存活更久,或者应该被转移给另一个对象,则可以将其存储在堆中。同时,控制权可以在协程之间双向传递 -向上或向下。协程增加了函数调用和函数类型的含义。int func(int arg)原型意味着一个名为func的函数,接收一个整数类型的参数arg,返回一个整数。类似的协程可能永远不会返回到其调用者,而调用者期望的值可能由另一个协程产生。让我们看看C++中是如何发生的。
C++中的协程设施
最初,你可以将它们视为智能指针。你已经知道它们是指针的包装器,并为内存管理提供额外的控制。协程以类似的方式工作,但围绕它们的代码更复杂。这次,我们需要一个函数原型的包装器。这个包装器将处理数据流和调度控制。包装器本身就是协程。我们定义了一个Task exCoroutine()任务(任务与Linux定义的任务不同) - 如果它使用以下三个操作符之一:co_await、co_yield或co_return,则被解释为协程。这里是一个例子:
#include <coroutine>
...
Task exCoroutine() {
co_return;
}
int main() { Task async_task = exCoroutine(); }
包装器类型当前是Task。它在调用者级别上是已知的。通过co_return操作符,协程对象被识别为exCoroutine()函数。创建Task类是系统程序员的工作。它不是标准库的一部分。那么Task类是什么?
struct Task {
struct promise_type {
Task get_return_object()
{ return {}; }
std::suspend_never initial_suspend()
{ return {}; }
std::suspend_never final_suspend() noexcept
{ return {}; }
void return_void() {}
void unhandled_exception() {}
};
};
重要提示
这是一个非常通用的模式,几乎在每个协程示例中都会使用。参考:https://en.cppreference.com/w/cpp/language/coroutines
我们称执行给定例程但不返回值的协程为任务。此外,协程与promise对象相关联 。promise对象在协程级别上被操纵。协程通过这个对象返回操作结果或引发异常。这个设施还需要协程帧(或协程状态),这是一个在堆上的内部对象,包含promise。它还由传递的参数组成 - 通过值复制,当前调用引用的表示;暂停点,以便协程相应地恢复;以及该点范围之外的局部变量。那么,我们的代码做了什么?嗯,从用户的角度来看,它什么也没做,但在后台发生了很多事情。让我们观察以下图表:
图2