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