在 C++20 之前,异步编程往往需要使用线程、回调或者第三方库(如 Boost.Asio、libuv 等)来实现。C++20 标准通过引入协程(coroutine)语法,提供了一种更直观、更高效的异步编程模型。本文将带你快速了解协程的核心概念、实现机制以及在实际项目中的应用场景。
1. 什么是协程?
协程是可以在执行过程中“挂起”和“恢复”的函数。与线程不同,协程在单个线程内切换,只占用少量栈空间,并且不需要像线程那样昂贵的上下文切换。协程可以被看作是把函数拆分成若干可暂停的步骤,每一步都可以被外部调度器控制。
- 挂起(
co_await、co_yield、co_return):函数在此处暂停执行,返回一个值或等待一个异步操作完成。 - 恢复:当外部调度器决定继续执行协程时,协程从挂起点恢复。
2. 协程的核心组件
C++20 协程的实现依赖于以下关键概念:
| 关键字 | 说明 | 典型用法 |
|---|---|---|
co_await |
等待一个异步操作完成,挂起协程 | auto result = co_await async_io(); |
co_yield |
产生一个值,挂起协程 | co_yield value; |
co_return |
结束协程,返回最终结果 | co_return final_value; |
std::suspend_always / std::suspend_never |
控制协程是否立即挂起 | co_await std::suspend_always{}; |
协程需要一个 promise type(承诺类型)来描述挂起、恢复以及返回值等行为。C++20 标准库提供了一些默认实现(如 std::promise、std::future),但在实际项目中我们通常会自定义 promise_type 以满足业务需求。
3. 一个简单的协程示例
下面的例子演示了如何使用协程实现一个异步计数器。它在每次计数后挂起,等待 1 秒钟后恢复,最终返回总计数值。
#include <iostream>
#include <coroutine>
#include <thread>
#include <chrono>
// 1. 定义一个简单的异步延迟类型
struct Delay {
struct promise_type {
std::chrono::milliseconds wait_time;
std::suspend_always initial_suspend() { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
void return_void() {}
void unhandled_exception() { std::terminate(); }
};
using handle_type = std::coroutine_handle <promise_type>;
handle_type coro;
Delay(std::chrono::milliseconds ms) : coro(handle_type::from_promise(*new promise_type{ms})) {}
~Delay() { coro.destroy(); }
void resume() {
std::this_thread::sleep_for(coro.promise().wait_time);
coro.resume();
}
};
// 2. 计数器协程
struct Counter {
struct promise_type {
int count = 0;
std::suspend_always initial_suspend() { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
int get_return_object() { return count; }
void return_void() {}
void unhandled_exception() { std::terminate(); }
};
using handle_type = std::coroutine_handle <promise_type>;
handle_type coro;
Counter(handle_type h) : coro(h) {}
~Counter() { coro.destroy(); }
int get_value() const { return coro.promise().count; }
// 计数函数
static Counter run(int max) {
for (int i = 1; i <= max; ++i) {
co_await Delay(1000ms); // 每秒等待一次
co_yield i; // 暂停并产生当前计数
co_await std::suspend_always{}; // 让外部恢复
}
co_return; // 结束协程
}
};
int main() {
auto counter = Counter::run(5);
while (counter.coro) {
counter.coro.resume(); // 恢复协程
std::cout << "Count: " << counter.get_value() << '\n';
}
std::cout << "Final count: " << counter.get_value() << '\n';
}
运行效果(每秒打印一次):
Count: 1
Count: 2
Count: 3
Count: 4
Count: 5
Final count: 5
该示例展示了协程挂起与恢复的基本流程。Delay 用来模拟异步等待,Counter 在每一次 co_yield 后挂起,外部通过 coro.resume() 恢复。
4. 协程在 IO 编程中的应用
协程最常用于网络 IO 或磁盘 IO。结合异步 I/O API(如 libuv、asio),可以让异步操作像同步代码一样编写。下面是一个使用 asio 的简化示例(不完整):
#include <asio.hpp>
#include <iostream>
asio::awaitable <void> tcp_client() {
asio::ip::tcp::socket socket(co_await asio::this_coro::executor);
co_await socket.async_connect(asio::ip::tcp::endpoint(asio::ip::make_address("127.0.0.1"), 8080), asio::use_awaitable);
std::string msg = "Hello, server!\n";
co_await asio::async_write(socket, asio::buffer(msg), asio::use_awaitable);
// 读取响应
std::array<char, 1024> buf;
std::size_t n = co_await asio::async_read(socket, asio::buffer(buf), asio::use_awaitable);
std::cout << "Received: " << std::string(buf.data(), n) << '\n';
socket.close();
}
优势
- 可读性:异步流程像同步代码,易于维护。
- 性能:避免线程上下文切换,协程本身非常轻量。
- 资源利用:单线程即可处理大量并发连接。
5. 常见坑与最佳实践
- 不要在协程里直接使用
std::thread:协程本身已经能处理并发,加入线程会增加复杂度。 - 确保
promise_type的生命周期:如果协程返回std::future或自定义类型,必须保证promise_type在协程结束后仍然有效。 - 异常处理:使用
unhandled_exception把异常转为std::terminate,或者自定义异常捕获逻辑。 - 调试难度:调试协程时,调试器可能会在挂起点停下,了解
awaitable的执行顺序非常重要。
6. 小结
C++20 协程为异步编程带来了革命性的简化。通过 co_await、co_yield 和 co_return,我们可以像写同步代码一样描述异步流程,极大提升代码可读性与可维护性。结合现有异步库(如 asio、libuv)或者自行实现轻量异步 I/O,协程在高性能网络服务、实时数据处理等领域已成为主流技术。
如果你正在寻找一种更高效、更易维护的异步编程方式,C++20 协程绝对值得一试。祝你编码愉快!