协程是 C++20 引入的新特性。
文章目录
- 基本概念
- std::coroutine_handle
- promise 类型
- co_yield
- 基本用法
- 优势
- 异步 TCP
- co_await
基本概念
协程(Coroutine)是一种比线程更加轻量级的并发编程模型。协程的调度由程序员手动控制。
异步不是并行,但是不阻塞~
协程的上下文切换是在用户态完成的,不需要陷入内核,因此切换速度非常快。
当协程遇到 co_await 时,它会保存当前的上下文信息,然后让出执行权,待异步操作完成后再恢复上下文继续执行。切换开销非常小,几乎可以忽略不计。
-
如果你在做异步编程(如网络 I/O、文件 I/O),用 co_await
-
如果你在实现生成器(如迭代器、流式数据处理),用 co_yield
std::coroutine_handle
协程的句柄,一个模板类——对协程进行控制与管理。
std::coroutine_handle<promise_type>;
主要方法:
-
resume()
:用于让协程恢复执行。
当协程处于挂起状态时,调用 resume() 方法,协程就会从挂起的位置接着执行。 -
done()
:用于检查协程是否已经执行完毕。
若协程执行完毕,done() 方法会返回 true;反之,则返回 false。 -
destroy()
:用于销毁协程,释放协程占用的资源。 -
promise()
:返回与协程关联的 promise 对象(struct promise_type),通过这个对象可以访问协程的状态和数据。
promise 类型
promise_type 对象定义了协程的行为和状态管理方式。
promise_type 结构体中的部分方法名字是固定的,协程框架会依据这些固定的方法名来调用相应的逻辑
固定的方法名:
get_return_object
创建并返回协程的返回对象yield_value
当协程里使用 【co_yield】 语句时,会调用此方法。它负责处理 co_yield 后面的值,并通过返回值决定协程是否要挂起。initial_suspend
在协程启动时会调用此方法,决定了协程在开始执行时是否要立即挂起。
【返回类型
】有 std::suspend_always(表示协程开始就挂起,控制权交还给调用者)和 std::suspend_never(表示协程开始就执行,co_yield 后接着执行)final_suspend
协程即将结束时会调用该方法,决定了协程在结束时是否要挂起。
返回同 initial_suspendunhandled_exception
当协程内部抛出未被内部捕获的异常时,会调用这个方法来处理异常。return_void
和return_value
处理协程返回值。
co_yield
co_yield 用于在协程中产生一个值,然后暂停协程的执行,将控制权交还给调用者。
这意味着协程可以在不同的时间点多次产生值,而不是像普通函数那样一次性返回一个结果。
当调用者再次请求协程继续执行时,协程会从 co_yield 之后的位置恢复执行。
避免了传统异步编程中的回调地狱问题。
Generator generate_numbers() {for (int i = 0; i < 5; ++i) {co_yield i; // 生成值并挂起}
}
每执行一次 co_yield i
,协程框架就会调用 Generator 类(当前协程函数的返回值类型)中 promise_type 结构体
的 yield_value 方法
,并且把 i 的值传递给该方法。
基本用法
简单生成器示例,生成从 0 到 4 的整数
#include <coroutine>
#include <iostream>
#include <cstdlib>// 生成器类定义
class Generator {
public: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 {}; } // 最终挂起void unhandled_exception() { std::exit(1); } // 异常处理std::suspend_always yield_value(int value) { // ※ 用于处理 co_yield 语句current_value = value;return {};}void return_void() {} // 处理 co_return 语句,这里的协程不返回值。};using handle_type = std::coroutine_handle<promise_type>;//协程的句柄,对协程进行控制与管理// 迭代器类struct Iterator {handle_type coro_handle;bool operator!=(const Iterator&) const { return !coro_handle.done(); }void operator++() { coro_handle.resume(); }int operator*() const { return coro_handle.promise().current_value; } // 获取值};// 构造函数和析构函数 : 接收/销毁 句柄explicit Generator(handle_type h) : coro_handle(h) {}~Generator() { if (coro_handle) coro_handle.destroy(); }// 支持范围循环Iterator begin() {if (coro_handle) coro_handle.resume();return Iterator{coro_handle};}Iterator end() { return Iterator{nullptr}; }private:handle_type coro_handle;
};// 协程函数
Generator generate_numbers() {for (int i = 0; i < 5; ++i) {co_yield i; // 生成值并挂起}
}// 使用示例
int main() {for (auto num : generate_numbers()) {std::cout << "Generated: " << num << std::endl;}return 0;
}
优势
- 异步非阻塞:
协程通过 co_await 挂起执行但不阻塞线程,可以在等待 I/O 时释放 CPU 资源
单线程即可处理大量并发 I/O 请求(如网络连接、文件读写)
像网络IO,磁盘IO等,CPU只是调度,真正执行的是硬件:网卡和磁盘。
所以程序中高IO的情况下,不想阻塞IO,就可以使用协程。
生命周期结束,协程可以正常进行。
IO异常后,协程也会抛出异常。
- 低开销
协程切换成本远低于线程切换(通常是纳秒级 vs 微秒级)。
异步 TCP
#include <boost/asio.hpp>
#include <boost/asio/use_awaitable.hpp>
#include <coroutine>
#include <iostream>namespace asio = boost::asio;
using asio::ip::tcp;// 协程异步连接和读写
asio::awaitable<void> async_client() { // 该协程不返回任何值try {auto executor = co_await asio::this_coro::executor; //获取当前协程的执行器,用于创建 tcp::socket 对象for (int i = 0; i < 5; ++i) {tcp::socket socket(executor);// 异步连接(非阻塞)co_await socket.async_connect( // 【异步连接】tcp::endpoint(asio::ip::make_address("127.0.0.1"), 8080),asio::use_awaitable // 协程挂起等, 控制权返回给事件循环,事件循环可以继续处理其他异步操作。);// 异步发送数据std::string request = "Hello from coroutine!";co_await asio::async_write(socket, asio::buffer(request), asio::use_awaitable); // 【异步发送】// 异步接收响应char response[1024];size_t len = co_await socket.async_read_some(asio::buffer(response), asio::use_awaitable); // 【异步接收】std::cout << "Received: " << std::string(response, len) << std::endl;}} catch (const std::exception& e) {std::cerr << "Error: " << e.what() << std::endl;}
}int main() {asio::io_context io_context; // Boost.Asio 的核心对象,负责管理所有的异步操作和事件循环。for (int i = 0; i < 5; ++i) {// 启动协程任务asio::co_spawn(io_context, async_client(), asio::detached); // (执行器, 协程函数, 协程任务独立运行 不等待)}// 运行事件循环io_context.run(); return 0;
}
co_await
关键字。让异步代码可以以同步的形式编写。
C++20 之前,没有 co_await 这个关键字,异步编程通常借助回调函数或者 std::future 与 std::promise 来实现,难以维护。
后面通常跟着一个可等待对象(Awaitable),这个对象需要实现特定的接口:
await_ready
判断可等待对象是否已经准备好await_suspend
当 await_ready 返回 false 时,await_suspend 方法会被调用。这个方法负责挂起协程,并安排在可等待对象完成时恢复协程的执行。await_resume
当可等待对象完成时,await_resume 方法会被调用。该方法的返回值会作为 co_await 表达式的结果返回给协程。
#include <iostream>
#include <coroutine>
#include <future>
#include <thread>// 定义一个可等待对象
struct Awaitable {bool await_ready() const noexcept { return false; }void await_suspend(std::coroutine_handle<> handle) const noexcept {std::thread([handle]() {std::this_thread::sleep_for(std::chrono::seconds(1));handle.resume();}).detach();}void await_resume() const noexcept {}
};// 协程函数
std::coroutine_handle<> coroutine_handle; // 协程句柄声明,这里没有使用
struct Task { // 协程函数的返回类型, 同“基本用法”的Generator类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() {}};
};Task coroutine_function() {std::cout << "Coroutine started." << std::endl;co_await Awaitable{};std::cout << "Coroutine resumed after waiting." << std::endl;
}int main() {coroutine_function();std::cout << "Main function continues." << std::endl;std::this_thread::sleep_for(std::chrono::seconds(2));return 0;
}