在C++20中,协程(coroutine)被正式纳入标准库,彻底改变了我们编写异步代码的方式。与传统的回调、Future/Promise或线程模型相比,协程提供了一种更自然、更直观的异步控制流。本文将带你从基础语法开始,深入理解协程的实现原理,并演示如何使用标准库中的std::experimental::generator和std::jthread配合std::future实现一个简单的异步IO示例。
1. 协程基础概念
1.1 什么是协程?
协程是一种“轻量级线程”,它可以在执行过程中挂起(co_await)并在之后恢复执行。与线程不同的是,协程在同一线程内切换,避免了上下文切换的高昂成本。
1.2 关键关键词
co_await:挂起当前协程并等待一个可等待对象完成。co_return:返回协程的最终值。co_yield:在生成器协程中生成一个值。std::coroutine_handle:协程句柄,用于手动控制协程。
1.3 协程的组成
struct coro {
struct promise_type { /* ... */ };
using handle_type = std::coroutine_handle <promise_type>;
};
promise_type是协程的核心,它定义了协程的生命周期回调(initial_suspend, final_suspend, return_value, unhandled_exception等)。协程的入口是promise_type::get_return_object(),它返回协程对象本身。
2. 标准库中的协程工具
C++20标准提供了以下协程相关工具:
| 工具 | 作用 | 典型用法 |
|---|---|---|
| `std::generator | ||
| 生成器协程 |for(auto v: generator{…})` |
||
std::suspend_always / std::suspend_never |
决定挂起行为 | return std::suspend_always{} |
| `std::future | ||
/std::async| 异步任务 |std::async([]{…})` |
此外,<experimental/coroutine>头文件包含了完整的协程实现,但在C++20正式版中已被移除,推荐使用<coroutine>。
3. 实战:异步文件读取
下面演示如何使用协程读取文件,模拟网络IO的异步读取模式。
3.1 需求描述
- 读取一个大文件,分块读取。
- 每读取一个块,立即返回给调用者。
- 使用协程让代码保持同步式的可读性。
3.2 代码实现
#include <coroutine>
#include <iostream>
#include <fstream>
#include <vector>
#include <string>
#include <optional>
#include <future>
#include <chrono>
#include <thread>
constexpr std::size_t BLOCK_SIZE = 4096;
// 1. 可等待对象:异步读取块
struct AsyncReadBlock {
std::ifstream& stream;
std::vector <char> buffer;
struct Awaiter {
AsyncReadBlock& op;
bool await_ready() const noexcept { return false; } // 永远挂起
void await_suspend(std::coroutine_handle<> h) noexcept {
// 异步模拟:在子线程中读取
std::thread([h, &op = op]() mutable {
if (!op.stream.read(op.buffer.data(), BLOCK_SIZE)) {
op.buffer.resize(op.stream.gcount());
}
h.resume(); // 读取完成后恢复协程
}).detach();
}
std::optional<std::size_t> await_resume() const noexcept {
if (op.buffer.empty()) return std::nullopt;
return op.buffer.size();
}
};
Awaiter operator co_await() noexcept { return Awaiter{*this}; }
};
// 2. 生成器协程:按块读取文件
std::generator<std::optional<std::size_t>> read_file_in_blocks(const std::string& path) {
std::ifstream file(path, std::ios::binary);
if (!file) co_return; // 文件打开失败
std::vector <char> buf(BLOCK_SIZE);
AsyncReadBlock readOp{file, buf};
while (true) {
std::optional<std::size_t> n = co_await readOp;
if (!n || *n == 0) co_return; // EOF
co_yield n; // 将块大小返回给调用者
buf.assign(BLOCK_SIZE, 0); // 清空缓冲区
}
}
// 3. 主函数
int main() {
std::string filename = "large_file.dat";
for (auto block_size : read_file_in_blocks(filename)) {
std::cout << "读取块大小: " << *block_size << " 字节" << std::endl;
// 这里可以对块进行处理,例如写入网络、解码等
}
std::cout << "文件读取完毕" << std::endl;
}
关键点说明
AsyncReadBlock负责把同步的文件读取包装成可等待对象。它在子线程中完成真正的IO,然后恢复协程。read_file_in_blocks使用std::generator协程,co_yield生成读取到的块大小。调用者可以像普通for循环一样遍历。- 线程安全:在协程挂起期间,子线程负责IO,主线程不受阻塞。若IO完成后需要共享数据,可通过
std::atomic或锁机制保证安全。
4. 性能与优势
- 低上下文切换成本:协程在同一线程切换,减少了线程切换开销。
- 简洁代码:异步流程像同步代码一样直观,易于维护。
- 可组合性:协程可以嵌套,使用
co_await链式调用,形成可组合的异步管道。
5. 进一步阅读与工具
- 官方标准草案:
N4861(C++20)对协程进行了详细说明。 - Boost.Coroutine2:在C++20之前的协程实验性实现。
- Asio:现代C++网络库,已将协程整合为核心特性,配合
asio::awaitable可轻松编写高性能网络应用。
小结
C++20协程为异步编程提供了最接近同步语法的解决方案。通过标准库中的生成器和可等待对象,配合多线程IO操作,你可以快速构建高效、可维护的异步应用。希望本文能帮助你在项目中有效利用协程技术,开启新的编程模式。