一:什么是协程
C++20 引入了协程(coroutine),这是 C++ 标准库中一个强大的新特性。协程是一种可以在执行中暂停并随后恢复的函数,允许程序在异步或并行场景下高效管理任务,而不需要传统的线程或复杂的回调机制。
协程是可以暂停其执行并保存其当前状态,稍后可以从该位置恢复执行的特殊函数。在某种程度上,协程类似于普通函数,但它们的执行流可以通过 co_await
、co_yield
或 co_return
来暂停和恢复。这与传统函数的行为不同,传统函数一旦开始执行,就会一直运行到返回或退出为止。
协程与线程的不同在于,协程不会引入新的线程,它们是在现有线程上执行的。协程的切换开销通常非常低,远小于线程切换,因此在处理高并发场景下具有很大的优势。
二:协程的语法
C++20 协程的语法引入了三种关键字:
co_return
:从协程中返回值。co_yield
:暂停协程,并返回一个值给调用者,稍后可以恢复协程。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() << " ";
}
}
四:协程的优势
- 简化异步代码:协程可以以同步风格编写异步代码,简化了复杂的异步操作,如异步 I/O、网络通信、文件读写等。
- 高性能:协程切换的开销通常比线程上下文切换低,因此在高并发场景下协程更为高效。
- 提高代码可读性:协程使得代码更加线性和易读,避免了大量嵌套的回调或状态机逻辑。