1. C++20协程:异步编程的革命性突破
在C++20标准发布之前,C++开发者处理异步操作时通常面临两种选择:要么陷入回调地狱,要么手动实现复杂的状态机。这两种方式都会导致代码难以维护和扩展。C++20引入的原生协程支持彻底改变了这一局面,让我们能够以同步代码的书写方式实现异步逻辑。
协程(Coroutine)是一种可以暂停执行并在之后恢复的函数。与普通函数不同,协程在挂起时会保持其局部状态,这使得编写异步代码变得异常简洁。想象一下,你正在阅读一本书,可以随时放下书签暂停阅读,之后又能从书签处继续——协程的工作方式与此类似。
2. 协程核心概念解析
2.1 协程的三大关键字
C++20协程引入了三个关键操作符,它们构成了协程的基础语法:
-
co_await:挂起当前协程,等待异步操作完成。当你在协程中调用co_await expr时,编译器会检查expr是否是一个"可等待"(awaitable)的对象。如果是,协程会暂停执行,直到等待的操作完成。 -
co_yield:挂起协程并返回一个值,主要用于生成器模式。每次调用co_yield都会产生一个新值,同时保持函数内部状态不变。 -
co_return:结束协程执行并返回最终结果。这与普通函数的return类似,但会触发协程的完成流程。
2.2 Promise类型:协程的控制中心
每个协程都与一个promise_type相关联,它定义了协程的行为方式。promise_type必须实现以下关键方法:
cpp复制struct promise_type {
// 协程开始时调用,返回协程的对外接口对象
Generator get_return_object();
// 控制协程初始是否挂起
std::suspend_always initial_suspend();
// 控制协程结束时是否挂起
std::suspend_always final_suspend() noexcept;
// 处理co_yield表达式
auto yield_value(T value);
// 处理co_return表达式
void return_void();
// 异常处理
void unhandled_exception();
};
promise_type就像是协程的"大脑",决定了协程在各种情况下的行为。例如,initial_suspend()返回std::suspend_always意味着协程创建后会立即挂起,而返回std::suspend_never则会让协程立即开始执行。
2.3 Awaitable接口:自定义等待行为
任何可以被co_await操作符使用的类型都必须满足Awaitable概念,即实现三个关键方法:
cpp复制struct MyAwaitable {
// 检查操作是否已经完成,无需真正挂起
bool await_ready();
// 挂起协程时调用,通常在这里启动异步操作
void await_suspend(std::coroutine_handle<> h);
// 协程恢复时调用,返回等待的结果
T await_resume();
};
通过自定义Awaitable对象,我们可以将各种异步操作(如I/O、定时器、线程任务等)无缝集成到协程中。
3. 构建Generator生成器
3.1 Generator的完整实现
Generator是协程最典型的应用场景之一,它允许我们以惰性求值的方式生成序列。下面是一个完整的Generator实现:
cpp复制template<typename T>
class Generator {
public:
struct promise_type {
std::optional<T> current_value;
std::exception_ptr exception;
Generator get_return_object() {
return Generator{*this};
}
std::suspend_always initial_suspend() { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
std::suspend_always yield_value(T value) {
current_value = std::move(value);
return {};
}
void return_void() { current_value.reset(); }
void unhandled_exception() { exception = std::current_exception(); }
};
using handle_type = std::coroutine_handle<promise_type>;
private:
handle_type handle_;
public:
explicit Generator(promise_type& p)
: handle_(handle_type::from_promise(p)) {}
~Generator() { if (handle_) handle_.destroy(); }
// 迭代器支持
struct iterator {
handle_type handle;
bool done;
iterator& operator++() {
handle.resume();
done = handle.done();
return *this;
}
T operator*() const { return *handle.promise().current_value; }
bool operator!=(const iterator& other) const { return done != other.done; }
};
iterator begin() {
if (!handle_) return iterator{nullptr, true};
handle_.resume();
return iterator{handle_, handle_.done()};
}
iterator end() { return iterator{nullptr, true}; }
};
这个实现包含了协程的所有关键组件:promise_type定义协程行为,coroutine_handle管理协程生命周期,迭代器支持使Generator可以用于range-based for循环。
3.2 Generator使用示例
有了Generator模板,我们可以轻松实现各种序列生成器:
cpp复制// 斐波那契数列生成器
Generator<long long> fibonacci(int n) {
long long a = 0, b = 1;
for (int i = 0; i < n; ++i) {
co_yield a;
auto temp = a;
a = b;
b = temp + b;
}
}
// 无限整数序列
Generator<int> integers(int start = 0) {
while (true) {
co_yield start++;
}
}
// 文件行读取器
Generator<std::string> read_lines(const std::string& filename) {
std::ifstream file(filename);
std::string line;
while (std::getline(file, line)) {
co_yield line;
}
}
使用这些生成器时,代码变得异常简洁:
cpp复制// 打印前10个斐波那契数
for (auto num : fibonacci(10)) {
std::cout << num << " ";
}
// 取前5个偶数斐波那契数
auto even_fibs = fibonacci(20)
| std::views::filter([](auto n) { return n % 2 == 0; })
| std::views::take(5);
for (auto num : even_fibs) {
std::cout << num << " ";
}
4. 实现异步Task
4.1 Task的完整实现
Generator适合生成器场景,而异步操作则需要另一种协程类型——Task。下面是Task的完整实现:
cpp复制template<typename T = void>
class Task {
public:
struct promise_type {
std::optional<T> value;
std::exception_ptr exception;
std::coroutine_handle<> continuation;
Task get_return_object() {
return Task{*this};
}
std::suspend_always initial_suspend() { return {}; }
auto final_suspend() noexcept {
struct awaiter {
promise_type* promise;
bool await_ready() noexcept { return false; }
void await_suspend(std::coroutine_handle<>) noexcept {
if (promise->continuation) {
promise->continuation.resume();
}
}
void await_resume() noexcept {}
};
return awaiter{this};
}
void return_value(T v) { value = std::move(v); }
void unhandled_exception() { exception = std::current_exception(); }
};
using handle_type = std::coroutine_handle<promise_type>;
handle_type handle_;
public:
explicit Task(promise_type& p) : handle_(handle_type::from_promise(p)) {}
~Task() { if (handle_) handle_.destroy(); }
bool await_ready() { return handle_.done(); }
void await_suspend(std::coroutine_handle<> h) {
handle_.promise().continuation = h;
handle_.resume();
}
T await_resume() {
if (handle_.promise().exception) {
std::rethrow_exception(handle_.promise().exception);
}
return *handle_.promise().value;
}
T get() {
handle_.resume();
return await_resume();
}
};
Task与Generator的主要区别在于:
- Task使用
co_await而不是co_yield - Task支持协程链式调用(通过continuation)
- Task通常用于一次性异步操作而非序列生成
4.2 使用Task实现异步HTTP请求
下面是一个模拟异步HTTP客户端的示例:
cpp复制class HttpClient {
public:
Task<std::string> get(const std::string& url) {
// 模拟异步I/O延迟
co_await std::suspend_always{};
// 实际实现中,这里会发起真正的异步请求
std::this_thread::sleep_for(std::chrono::milliseconds(100));
co_return "Response from " + url;
}
};
// 顺序执行多个请求
Task<void> fetch_user_data() {
HttpClient client;
auto profile = co_await client.get("https://api.example.com/profile");
auto orders = co_await client.get("https://api.example.com/orders");
auto preferences = co_await client.get("https://api.example.com/preferences");
std::cout << "Profile: " << profile << std::endl;
std::cout << "Orders: " << orders << std::endl;
std::cout << "Preferences: " << preferences << std::endl;
}
// 并行执行多个请求
Task<void> fetch_parallel() {
HttpClient client;
auto profile_task = client.get("https://api.example.com/profile");
auto orders_task = client.get("https://api.example.com/orders");
auto prefs_task = client.get("https://api.example.com/preferences");
auto profile = co_await profile_task;
auto orders = co_await orders_task;
auto prefs = co_await prefs_task;
// 处理结果...
}
虽然代码看起来是顺序执行的,但实际上每个co_await都会挂起当前协程,在异步操作完成后再恢复执行。这使得我们可以用同步代码的风格编写异步逻辑,同时保持高性能。
5. 协程调度器设计
5.1 简单调度器实现
要充分发挥协程的威力,通常需要一个调度器来管理协程的执行。下面是一个简单的调度器实现:
cpp复制class Scheduler {
std::queue<std::coroutine_handle<>> tasks_;
std::mutex mutex_;
public:
void schedule(std::coroutine_handle<> task) {
std::lock_guard<std::mutex> lock(mutex_);
tasks_.push(task);
}
void run() {
while (true) {
std::coroutine_handle<> task;
{
std::lock_guard<std::mutex> lock(mutex_);
if (tasks_.empty()) break;
task = tasks_.front();
tasks_.pop();
}
task.resume();
}
}
};
struct ScheduleAwaiter {
Scheduler& scheduler;
bool await_ready() { return false; }
void await_suspend(std::coroutine_handle<> h) {
scheduler.schedule(h);
}
void await_resume() {}
};
Task<void> async_task(Scheduler& sched) {
std::cout << "Task started on thread " << std::this_thread::get_id() << "\n";
co_await ScheduleAwaiter{sched};
std::cout << "Task resumed on thread " << std::this_thread::get_id() << "\n";
co_await ScheduleAwaiter{sched};
std::cout << "Task finished on thread " << std::this_thread::get_id() << "\n";
}
int main() {
Scheduler sched;
auto task1 = async_task(sched);
auto task2 = async_task(sched);
sched.run();
}
这个调度器虽然简单,但展示了协程调度的基本原理。在实际应用中,你可能会需要更复杂的调度策略,比如:
- 多线程工作窃取
- 优先级调度
- I/O事件集成
5.2 协程与现有异步框架集成
现代C++项目通常会使用现有的异步框架(如Boost.Asio、libuv等)。我们可以通过自定义Awaitable将这些框架与C++20协程集成:
cpp复制// Boost.Asio集成示例
template<typename CompletionToken>
auto async_read_some(asio::ip::tcp::socket& socket,
asio::mutable_buffer buffer,
CompletionToken&& token) {
return asio::async_initiate<CompletionToken, void(std::error_code, size_t)>(
[&](auto handler) {
socket.async_read_some(buffer, std::move(handler));
}, token);
}
struct AsioAwaitable {
asio::ip::tcp::socket& socket;
asio::mutable_buffer buffer;
bool await_ready() { return false; }
void await_suspend(std::coroutine_handle<> h) {
async_read_some(socket, buffer,
[h](std::error_code ec, size_t bytes) mutable {
if (ec) {
// 错误处理...
}
h.resume();
});
}
size_t await_resume() { return bytes_transferred_; }
private:
size_t bytes_transferred_;
};
Task<void> handle_connection(asio::ip::tcp::socket socket) {
std::array<char, 1024> buffer;
while (true) {
size_t n = co_await AsioAwaitable{socket, asio::buffer(buffer)};
// 处理接收到的数据...
}
}
这种集成方式允许我们在享受协程简洁语法的同时,继续利用成熟异步框架的功能和性能。
6. 协程性能优化与陷阱规避
6.1 协程内存分配优化
默认情况下,协程帧(存储局部变量和挂起状态的内存)是通过operator new分配的。对于性能敏感的场景,我们可以自定义内存分配:
cpp复制struct custom_coroutine_memory {
static void* operator new(size_t size) {
if (auto ptr = my_memory_pool.allocate(size)) {
return ptr;
}
return ::operator new(size);
}
static void operator delete(void* ptr, size_t size) {
if (my_memory_pool.deallocate(ptr, size)) {
return;
}
::operator delete(ptr);
}
};
Task<void, custom_coroutine_memory> optimized_task() {
// 这个协程将使用自定义内存分配器
co_return;
}
6.2 常见陷阱与解决方案
-
协程生命周期管理:
- 问题:协程可能在挂起时被意外销毁
- 解决方案:使用RAII包装coroutine_handle
- 示例:
cpp复制struct ScopedCoroutine { std::coroutine_handle<> handle; ~ScopedCoroutine() { if (handle) handle.destroy(); } };
-
异常安全:
- 问题:协程中的异常可能被忽略
- 解决方案:在promise_type中妥善处理unhandled_exception
- 示例:
cpp复制void unhandled_exception() { exception_ = std::current_exception(); }
-
递归深度限制:
- 问题:协程相互等待可能导致栈溢出
- 解决方案:使用调度器进行尾调用优化
- 示例:
cpp复制void await_suspend(std::coroutine_handle<> h) { scheduler.schedule(h); // 而不是直接resume() }
6.3 协程与多线程
协程本身不是线程,但可以在多线程环境中使用。关键注意事项:
- 协程挂起后可能在不同线程恢复,需要确保线程安全
- 避免在协程中使用线程局部存储(TLS)
- 使用原子操作或互斥锁保护共享数据
cpp复制Task<void> thread_safe_task(std::shared_ptr<ThreadSafeData> data) {
auto local_copy = co_await data->async_fetch();
{
std::lock_guard<std::mutex> lock(data->mutex);
data->process(local_copy);
}
co_return;
}
7. ��程在实际项目中的应用
7.1 游戏开发中的协程应用
游戏开发中经常需要处理复杂的时序逻辑,协程是完美的解决方案:
cpp复制Generator<float> animate_position(Vector3f start, Vector3f end, float duration) {
float progress = 0.0f;
while (progress < 1.0f) {
auto current = start + (end - start) * progress;
object.set_position(current);
co_yield progress;
progress += delta_time / duration;
}
object.set_position(end);
}
// 使用示例
auto anim1 = animate_position({0,0,0}, {10,0,0}, 2.0f);
auto anim2 = animate_position({0,0,0}, {0,10,0}, 1.5f);
while (!anim1.done() || !anim2.done()) {
if (!anim1.done()) anim1.next();
if (!anim2.done()) anim2.next();
render_frame();
}
7.2 网络服务中的协程应用
网络服务可以借助协程简化复杂的异步逻辑:
cpp复制Task<void> handle_client(asio::ip::tcp::socket socket) {
try {
asio::streambuf buffer;
while (true) {
size_t n = co_await async_read_until(socket, buffer, '\n');
std::string line{
asio::buffers_begin(buffer.data()),
asio::buffers_begin(buffer.data()) + n - 1};
buffer.consume(n);
auto response = co_await process_request_async(line);
co_await async_write(socket, asio::buffer(response + "\n"));
}
} catch (const std::exception& e) {
log_error(e.what());
}
}
Task<void> server_main() {
auto executor = co_await asio::this_coro::executor;
asio::ip::tcp::acceptor acceptor(executor,
asio::ip::tcp::endpoint(asio::ip::tcp::v4(), 8080));
while (true) {
auto socket = co_await acceptor.async_accept();
co_spawn(executor,
handle_client(std::move(socket)),
asio::detached);
}
}
7.3 协程与现有设计模式结合
协程可以与许多现有设计模式完美结合:
-
生产者-消费者模式:
cpp复制Generator<Data> producer() { while (auto item = get_next_item()) { co_yield item; } } Task<void> consumer() { auto items = producer(); while (auto item = co_await items.next()) { process(*item); } } -
观察者模式:
cpp复制Generator<Event> event_stream(EventSource& source) { auto subscriber = source.subscribe(); while (true) { co_yield co_await subscriber.next_event(); } } -
状态模式:
cpp复制Generator<State> state_machine() { State current = initial_state; while (current != terminal_state) { co_yield current; current = co_await transition(current); } }
8. C++协程的未来发展
C++20只是协程支持的开始,未来的C++标准将继续增强协程功能:
-
C++23中的新特性:
- std::generator标准库类型
- 更完善的任务协程支持
- 协程工具库(如协程traits)
-
C++26的预期改进:
- 协程堆内存分配的进一步优化
- 协程与执行器的深度集成
- 可能引入协程取消功能
-
第三方库的协程支持:
- 更多网络库、GUI框架将提供原生协程接口
- 协程调试工具的完善
- 跨语言协程交互(如C++与Python协程互操作)
在实际项目中采用协程时,建议:
- 从小的、独立的模块开始试点
- 建立协程编码规范和最佳实践
- 为团队提供必要的培训和支持
- 逐步将协程集成到现有架构中
