C++ 语言特性29 - 协程介绍

一:什么是协程

C++20 引入了协程(coroutine),这是 C++ 标准库中一个强大的新特性。协程是一种可以在执行中暂停并随后恢复的函数,允许程序在异步或并行场景下高效管理任务,而不需要传统的线程或复杂的回调机制。

协程是可以暂停其执行并保存其当前状态,稍后可以从该位置恢复执行的特殊函数。在某种程度上,协程类似于普通函数,但它们的执行流可以通过 co_awaitco_yieldco_return 来暂停和恢复。这与传统函数的行为不同,传统函数一旦开始执行,就会一直运行到返回或退出为止。

协程与线程的不同在于,协程不会引入新的线程,它们是在现有线程上执行的。协程的切换开销通常非常低,远小于线程切换,因此在处理高并发场景下具有很大的优势。

二:协程的语法

C++20 协程的语法引入了三种关键字:

  1. co_return:从协程中返回值。
  2. co_yield:暂停协程,并返回一个值给调用者,稍后可以恢复协程。
  3. co_await:暂停协程,等待某个异步操作完成后再继续执行。

为了使用协程,C++ 中的一个普通函数必须返回某种特殊的类型,而不是像 void 或普通类型那样。这个特殊的返回类型需要符合协程的约定,它定义了如何控制协程的生命周期。下面举一个例子,演示下协程的使用:

cpp 复制代码
#include <iostream>
#include <coroutine>
#include <thread>
#include <chrono>

// 一个简单的协程返回对象类型
struct Task {
    struct promise_type {
        Task get_return_object() {
            return Task{std::coroutine_handle<promise_type>::from_promise(*this)};
        }
        std::suspend_never initial_suspend() { return {}; }  // 协程立即开始执行
        std::suspend_always final_suspend() noexcept { return {}; } // 协程结束时暂停
        void return_void() {}
        void unhandled_exception() { std::terminate(); }
    };

    std::coroutine_handle<promise_type> handle;

    Task(std::coroutine_handle<promise_type> h) : handle(h) {}
    ~Task() { handle.destroy(); }

    void resume() { handle.resume(); }
};

// 一个自定义的 awaitable 类型,用于模拟异步任务
struct Awaiter {
    bool await_ready() { return false; } // 表示协程需要挂起
    void await_suspend(std::coroutine_handle<>) {
        // 模拟异步任务的延迟,例如 I/O 操作
        std::this_thread::sleep_for(std::chrono::seconds(1));
    }
    void await_resume() {}
};

// 协程函数,模拟异步任务
Task async_task() {
    std::cout << "Task started, waiting for 1 second...\n";
    co_await Awaiter{};  // 挂起协程并等待模拟异步任务
    std::cout << "Task resumed after 1 second!\n";
}

int main() {
    Task task = async_task();
    task.resume();  // 启动协程
    std::cout << "Main function continues execution...\n";
}

上面代码中主要变量和函数如下;

  • 协程句柄 (std::coroutine_handle)

    std::coroutine_handle 是一个可以用来控制协程生命周期的类型。它可以暂停、恢复协程以及销毁协程。std::coroutine_handle 提供了一些常用的成员函数,例如 resume()destroy() 等,用于管理协程的执行。

  • Promise 对象

    每个协程都有一个与之关联的 promise 对象(promise_type )。它负责管理协程的状态和返回值。promise_type 需要实现一些特定的函数,比如 get_return_object()(返回协程的返回对象)、initial_suspend()(定义协程开始时的行为)和 final_suspend()(定义协程结束时的行为)。

  • 挂起点 (co_await)

    co_await 是协程的核心,它允许协程暂停执行,等待某个条件满足时恢复。co_await 的行为依赖于其后跟随的对象或表达式,它可以是等待一个异步操作完成,也可以是一个延迟或其他触发条件。

    协程的暂停和恢复类似于保存函数的状态,并在以后恢复它的执行,这样可以避免使用回调或复杂的状态机来处理异步操作

  • 协程函数 async_task

    • 使用 co_await 关键字等待 Awaiter 类型,这会暂停协程,并在一秒钟后恢复。
    • 恢复后,协程继续执行并打印消息。
  • 自定义的 Awaiter

    • await_ready 返回 false,表明协程需要挂起。
    • await_suspend 中使用 std::this_thread::sleep_for 来模拟一个异步任务(实际情况中可以是 I/O 操作或其他异步任务)。
    • await_resume 恢复协程。
  • main 函数

    • 调用 async_task 来创建协程对象 task,并使用 resume 来启动协程。

这个例子运行过程如下:

  • 协程开始执行,打印 Task started, waiting for 1 second...,然后协程挂起 1 秒钟。
  • 主函数继续执行并打印 Main function continues execution...
  • 1 秒钟后,协程恢复执行并打印 Task resumed after 1 second!

三:协程的使用场景

1. 异步 I/O 操作

在传统的同步代码中,I/O 操作(如网络或磁盘操作)通常会阻塞线程,导致线程空闲等待。在 C++ 中使用协程,可以使得这些 I/O 操作异步化,协程可以在 I/O 操作完成时继续执行,而不阻塞整个线程。在这个例子中,协程 sleep_one_second 会暂停执行 1 秒,然后恢复。

cpp 复制代码
#include <iostream>
#include <chrono>
#include <thread>
#include <coroutine>

struct Sleeper {
    struct promise_type {
        Sleeper get_return_object() { return {}; }
        std::suspend_always initial_suspend() { return {}; }
        std::suspend_always final_suspend() noexcept { return {}; }
        void return_void() {}
        void unhandled_exception() { std::terminate(); }
    };

    bool await_ready() { return false; }
    void await_suspend(std::coroutine_handle<>) {
        std::this_thread::sleep_for(std::chrono::seconds(1));
    }
    void await_resume() {}
};

Sleeper sleep_one_second() {
    std::cout << "Sleeping..." << std::endl;
    co_await Sleeper{};
    std::cout << "Awake!" << std::endl;
}
2. 生成器

协程可以用来实现生成器模式,允许在迭代过程中动态生成值,而不需要一次性返回整个集合。这个例子中的 range 协程会生成一个从 0 到 4 的整数序列,类似于 Python 的生成器。

cpp 复制代码
#include <iostream>
#include <coroutine>
#include <vector>

struct Generator {
    struct promise_type {
        int current_value;

        Generator get_return_object() { return Generator{std::coroutine_handle<promise_type>::from_promise(*this)}; }
        std::suspend_always initial_suspend() { return {}; }
        std::suspend_always final_suspend() noexcept { return {}; }
        std::suspend_always yield_value(int value) {
            current_value = value;
            return {};
        }
        void return_void() {}
        void unhandled_exception() { std::terminate(); }
    };

    std::coroutine_handle<promise_type> handle;

    Generator(std::coroutine_handle<promise_type> h) : handle(h) {}
    ~Generator() { if (handle) handle.destroy(); }

    bool move_next() {
        handle.resume();
        return !handle.done();
    }

    int current_value() { return handle.promise().current_value; }
};

Generator range(int start, int end) {
    for (int i = start; i < end; ++i) {
        co_yield i;
    }
}

int main() {
    auto gen = range(0, 5);
    while (gen.move_next()) {
        std::cout << gen.current_value() << " ";
    }
}

四:协程的优势

  1. 简化异步代码:协程可以以同步风格编写异步代码,简化了复杂的异步操作,如异步 I/O、网络通信、文件读写等。
  2. 高性能:协程切换的开销通常比线程上下文切换低,因此在高并发场景下协程更为高效。
  3. 提高代码可读性:协程使得代码更加线性和易读,避免了大量嵌套的回调或状态机逻辑。
相关推荐
源码哥_博纳软云13 分钟前
JAVA同城服务场馆门店预约系统支持H5小程序APP源码
java·开发语言·微信小程序·小程序·微信公众平台
学会沉淀。21 分钟前
Docker学习
java·开发语言·学习
ragnwang38 分钟前
C++ Eigen常见的高级用法 [学习笔记]
c++·笔记·学习
西猫雷婶1 小时前
python学opencv|读取图像(二十一)使用cv2.circle()绘制圆形进阶
开发语言·python·opencv
kiiila1 小时前
【Qt】对象树(生命周期管理)和字符集(cout打印乱码问题)
开发语言·qt
小_太_阳1 小时前
Scala_【2】变量和数据类型
开发语言·后端·scala·intellij-idea
直裾1 小时前
scala借阅图书保存记录(三)
开发语言·后端·scala
唐 城2 小时前
curl 放弃对 Hyper Rust HTTP 后端的支持
开发语言·http·rust
码银3 小时前
【python】银行客户流失预测预处理部分,独热编码·标签编码·数据离散化处理·数据筛选·数据分割
开发语言·python
从善若水3 小时前
【2024】Merry Christmas!一起用Rust绘制一颗圣诞树吧
开发语言·后端·rust