首页 > 编程笔记 > C++笔记 阅读:66

什么是协程,C++协程快速入门教程(附带实例)

协程是一种程序组件,用于更高级地控制程序的执行流程,使得能够以更接近人类思维的方式进行编程,尤其在处理需要暂停和恢复执行的场景时。

协程与传统的函数不同,能在执行中暂停(挂起),并在需要的时候从暂停点恢复(继续)执行。这种能力允许协程保持其执行状态(包括局部变量和程序指针等),直到它们再次被激活。

协程的这种行为类似于电子游戏中的“保存进度”和“继续游戏”的功能,允许玩家在停止点保存游戏状态,之后可以从相同的地方继续游戏。

协程具有以下功能:

协程与线程的对比

在理解协程和传统的线程时,关键在于区分它们在设计、使用成本和适用场景上的差异。这有助于开发者更好地把握两者的使用时机,并根据具体需求选择合适的并发模型。

1) 设计哲学与实现

线程是一种重量级的操作系统级功能,由操作系统调度,并可能在多个处理器上并行执行。每个线程都是一个独立的执行路径,拥有自己的调用栈、程序计数器和其他系统资源。

协程则是一种轻量级的用户态构造,它们寄生在线程之中,并由应用程序或运行时库来管理。协程共享其宿主线程的资源,如调用栈和程序状态,这使得协程之间的切换成本远低于线程之间的切换成本。

2) 开销与效率

线程的创建和上下文切换是资源密集型的操作,特别是当系统运行大量线程时,这些开销可能会显著影响系统性能。相比之下,协程提供了一种更为高效的并发执行模式。协程之间的切换主要涉及寄存器的简单变更,不像线程切换那么复杂和耗时。

3) 适用性与灵活性

对于计算密集型任务或需要真正并行处理的场景,线程是一个非常合适的选择。然而,在面对 I/O 密集型或要求高并发的网络应用时,过多的线程可能会耗尽系统资源,影响性能。

在这种情况下,协程以其低开销和高效的上下文切换优势,成为更理想的选择。它们能够在不增加额外硬件负担的情况下,显著提升应用的响应速度和处理能力。

C++20中的协程

在 C++20 中,协程被实现为“无栈式”(stackless),这意味着它们的状态、局部变量和必要信息不是存储在传统的调用栈上,而是放置在一个堆上分配的数据结构中,被称为“协程帧”(coroutine frame)。这种设计避免了在传统调用栈上保存状态的需求,从而减少了栈溢出的风险,并允许协程在需要时被挂起和恢复。

协程的挂起通过 co_await 表达式实现,此时控制权返回到调用者或恢复者。当协程被挂起时,其当前执行状态被保存在协程帧中。这使得协程可以在稍后某个时间点,如 I/O 操作完成时,通过协程句柄重新恢复执行,从而继续从上次停止的地方执行。

在 C++ 中使用协程时,需要包括 <coroutine> 头文件并指定协程库,比如在 GCC 编译器中加上 -fcoroutines 选项。

协程库之所以设计为独立链接,是为了让开发者可以根据需求决定是否使用协程功能,避免将不必要的代码加入最终产品中,从而减轻程序负担、缩小体积,并提高效率。

此外,这种设计也增加了灵活性和可优化性,使得协程库能够针对不同应用场景进行特定的性能优化。

C++20通过引入几个关键字和库来实现协程的功能:
协程库中的常用组件如下:
接下来,将详细探讨 C++20 中协程的生命周期管理。这一部分将专注于如何通过协程的创建、暂停、恢复到终止过程中,系统地管理协程的生命期,以确保资源的有效利用和程序的稳定运行。

C++ 20协程的生命周期管理

C++20 的协程生命周期管理主要涉及以下几个关键阶段:

1) 创建和启动

协程的生命周期开始于被调用时的创建。一旦一个函数被声明为协程(通常通过包含 co_await、co_yield 或 co_return 关键字),编译器将自动处理协程所需的设置工作,包括为协程分配一个协程帧,并初始化 promise_type 对象。

promise_type 对象在协程的整个生命周期中扮演着核心角色,负责初始化和维护协程状态。协程的实际运行由协程句柄(std::coroutine_handle<>)触发,该句柄管理着协程的入口和恢复点。

2) 暂停与挂起

协程在执行过程中,可以通过 co_await 表达式在等待异步操作的完成时自动暂停,将执行权交还给调用者或恢复者。协程的状态(包括局部变量和执行点)将保存在协程帧中。这一机制允许协程在非阻塞的情况下等待,从而不占用宝贵的系统资源。

3) 恢复执行

当协程等待的条件被满足时,如 I/O 操作完成或定时器被触发,协程可以通过其句柄被重新激活。此时,协程从上次暂停的地方恢复执行,所有的状态信息都会从协程帧中恢复。

4) 结束与清理

当协程达到 co_return 表达式或正常结束其逻辑时,协程将进入清理阶段。

在这个阶段,promise_type 对象会处理任何必要的状态终结工作,包括返回值的传递和异常的处理。一旦协程完成所有操作,其协程帧和其他资源会被释放,标志着协程生命周期结束。

通过这样的生命周期管理,C++20 的协程不仅优化了资源的使用,还提高了程序的响应性和并发性能。理解并掌握这些生命周期阶段对于开发高效和可维护的异步 C++ 应用程序至关重要。

C++ 20协程实例

假设有一个需要从多个数据源异步加载数据的应用场景。传统的方法可能需要使用多线程或者异步回调,这样会增加代码的复杂性和维护难度。使用协程能以接近同步代码的方式书写异步逻辑,从而使得代码更加直观和易于理解。
#include <coroutine>
#include <iostream>
#include <memory>
#include <thread>
#include <chrono>
#include <optional>
// 协程返回类型定义
struct Task {
    // Promise 类型定义协程的行为和返回值
    struct promise_type {
        std::optional<int> value;  // 存储协程返回的值,用optional 包装以支持延迟赋值
        // 初始挂起,协程启动时挂起,等待显式恢复
        std::suspend_always initial_suspend() { return {}; }
        // 最终挂起,协程结束时挂起,防止协程退出后立即销毁资源
        std::suspend_always final_suspend() noexcept { return {}; }
        // 获取协程的返回对象,链接协程与外部世界
        Task get_return_object() { return Task{this}; }
        // 设置协程的返回值
        void return_value(int v) { value = v; }
        // 异常处理,若协程内部抛出未捕获异常,则结束程序
        void unhandled_exception() { std::exit(1); }
        // 生成暂停点并返回值给调用者,再次挂起协程
        auto yield_value(int v) {
            value = v;
            return std::suspend_always{};
        }
    };
    // 尝试恢复协程的执行,如果协程已完成,则返回 false
    bool resume() {
        if (handle.done()) {
            return false;
        }
        handle.resume();
        return true;
    }
    // 获取协程处理完成后的结果
    int get() {
        return *handle.promise().value;
    }
    // 构造函数和析构函数管理协程的生命周期
    Task(promise_type* p) :handle(std::coroutine_handle<promise_type>::from_promise(*p)) {}
    ~Task() {
        if (handle) handle.destroy();
    }
    private:
        std::coroutine_handle<promise_type> handle;  // 内部保存对应协程的句柄
};
// 异步函数定义,模拟长时间的数据处理任务
Task loadDataAsync(int data) {
    // 模拟耗时操作
    std::this_thread::sleep_for(std::chrono::seconds(2));
    // 通过 co_return 语句返回处理结果,协程在此挂起并将控制权交还给调用者
    co_return data * 2;
}
int main() {
    // 启动异步任务
    auto dataLoader = loadDataAsync(10);
    // 同时执行其他任务
    std::cout << "Doing other work while waiting for data..." << std::endl;
    // 循环调用resume 直到协程执行完成
    while (dataLoader.resume());
    // 获取异步任务的结果并打印
    int result = dataLoader.get();
    std::cout << "Data loaded: " << result << std::endl;
    return 0;
} 
在这个示例中,使用了 C++20 的协程特性来异步处理数据加载任务。下面将逐步解析示例代码的关键部分,以帮助理解协程的工作原理。

1) 自定义的 Task 类型是构建协程的基础。它定义了协程的行为,特别是如何启动、暂停、结束以及处理返回值。

promise_type 结构体是协程的核心,负责管理协程的状态和返回值。它包括以下几个重要的成员函数:
2) 异步函数 loadDataAsync 是一个返回 Task 类型的函数,它模拟一个耗时的数据加载过程,使用 std::this_thread::sleep_for 来模拟数据处理的延迟。

co_return 这个关键字结束协程并将结果传回。在本例中,通过 co_return data * 2; 返回输入数据的两倍。这里的操作被异步执行。

3) 主函数展示了如何使用协程进行异步编程:
这个示例说明协程在保持代码逻辑清晰的同时,能有效地管理和同步异步任务。通过协程,我们能以几乎同步的方式编写代码来执行异步操作,这对于复杂的程序设计尤其有益。

相关文章