在传统的 C++ 异步编程中,回调函数、状态机、以及手动管理资源往往让代码变得臃肿且难以维护。自从 C++20 引入协程(coroutine)以来,编写清晰、直观的异步代码变得前所未有地简单。本文将从协程的基本概念、关键字到实际示例,详细阐述如何利用 C++20 协程实现高效的异步 I/O 与任务调度。
1. 协程的核心概念
1.1 协程与线程的区别
- 线程:操作系统级别的并发单元,切换开销大。
- 协程:用户级的轻量级协作式调度,切换成本极低,只需要保存和恢复堆栈指针。
1.2 协程的基本术语
| 术语 | 定义 |
|---|---|
promise_type |
协程函数返回值类型的内部实现,用来传递状态与异常。 |
suspend_always / suspend_never |
控制协程的挂起与恢复行为。 |
awaitable |
表示可以被 co_await 的对象。 |
2. C++20 协程的语法要点
co_return // 返回值,转交给 promise_type
co_yield // 在 coroutine 中产生值
co_await // 等待 awaitable 对象完成
2.1 协程函数定义
std::future <int> asyncAdd(int a, int b) {
co_return a + b;
}
std::future内部实现了协程的promise_type,因此直接使用即可。
2.2 自定义 awaitable
struct Waitable {
std::chrono::milliseconds ms;
bool await_ready() const noexcept { return ms.count() == 0; }
void await_suspend(std::coroutine_handle<> h) const noexcept {
std::thread([=, h]() {
std::this_thread::sleep_for(ms);
h.resume(); // 继续协程
}).detach();
}
void await_resume() const noexcept {}
};
使用方式:
async void example() {
std::cout << "Start\n";
co_await Waitable{ std::chrono::milliseconds(1000) };
std::cout << "After 1s\n";
}
3. 实战:异步文件读取
#include <iostream>
#include <filesystem>
#include <fstream>
#include <coroutine>
#include <vector>
#include <string>
#include <thread>
struct FileReadAwaitable {
std::string path;
std::vector <char> buffer;
bool await_ready() const noexcept { return false; }
void await_suspend(std::coroutine_handle<> h) const noexcept {
std::thread([=, h]() {
std::ifstream file(path, std::ios::binary);
if (file) {
file.seekg(0, std::ios::end);
size_t size = file.tellg();
buffer.resize(size);
file.seekg(0, std::ios::beg);
file.read(buffer.data(), size);
}
h.resume();
}).detach();
}
const std::vector <char>& await_resume() const noexcept { return buffer; }
};
struct AsyncFileReader {
struct promise_type {
AsyncFileReader get_return_object() {
return AsyncFileReader{
std::coroutine_handle <promise_type>::from_promise(*this)};
}
std::suspend_always initial_suspend() { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
void return_void() {}
void unhandled_exception() { std::terminate(); }
};
std::coroutine_handle <promise_type> handle;
~AsyncFileReader() {
if (handle) handle.destroy();
}
};
AsyncFileReader read_file_async(const std::string& path) {
FileReadAwaitable awaitable{path, {}};
auto buffer = co_await awaitable;
std::cout << "File size: " << buffer.size() << " bytes\n";
co_return;
}
int main() {
auto reader = read_file_async("example.txt");
// 在主线程中做其他事情...
std::this_thread::sleep_for(std::chrono::seconds(2));
return 0;
}
说明
FileReadAwaitable将文件读取包装为 awaitable,后台线程完成 I/O,然后在主线程恢复协程。AsyncFileReader通过promise_type管理协程生命周期,保证资源得到正确释放。
4. 协程与任务调度器
如果想让多个协程并行执行并共享线程池,可以使用简单的任务调度器。
#include <queue>
#include <mutex>
#include <condition_variable>
#include <vector>
#include <thread>
class ThreadPool {
public:
ThreadPool(size_t n) : stop(false) {
workers.reserve(n);
for (size_t i = 0; i < n; ++i)
workers.emplace_back([this] { this->worker(); });
}
~ThreadPool() {
{
std::unique_lock lock(m);
stop = true;
cv.notify_all();
}
for (auto& t : workers) t.join();
}
template<class F> void enqueue(F&& f) {
{
std::unique_lock lock(m);
tasks.emplace(std::forward <F>(f));
}
cv.notify_one();
}
private:
void worker() {
while (true) {
std::function<void()> task;
{
std::unique_lock lock(m);
cv.wait(lock, [this]{ return stop || !tasks.empty(); });
if (stop && tasks.empty()) return;
task = std::move(tasks.front());
tasks.pop();
}
task();
}
}
std::vector<std::thread> workers;
std::queue<std::function<void()>> tasks;
std::mutex m;
std::condition_variable cv;
bool stop;
};
协程与线程池结合
// 将 awaitable 的 await_suspend 改为将恢复操作提交到线程池
struct AsyncSleep {
std::chrono::milliseconds ms;
ThreadPool& pool;
void await_suspend(std::coroutine_handle<> h) const noexcept {
pool.enqueue([=, h]() {
std::this_thread::sleep_for(ms);
h.resume();
});
}
};
5. 性能与调优
| 方面 | 建议 |
|---|---|
| 堆栈大小 | 协程默认使用 8KB 的栈,若递归深度大可手动指定更大堆栈。 |
| 异常处理 | promise_type::unhandled_exception 可以捕获并转换为 std::exception_ptr,防止程序崩溃。 |
| 资源清理 | 使用 final_suspend 返回 std::suspend_always,确保协程在结束前有机会执行清理代码。 |
6. 结语
C++20 协程为异步编程提供了强大的语义层次与简洁语法,使得复杂的异步逻辑可以像同步代码一样书写。虽然协程本身的底层实现仍然需要注意资源管理和线程调度,但只要掌握基本模式,即可在网络、文件 I/O、定时任务等领域快速构建高性能、可维护的系统。希望本文能帮助你在日常项目中更好地运用协程,解锁 C++ 异步编程的无限潜能。