C++20正式引入协程支持后,C++17已出现了许多协程的实验性实现。协程通过“挂起”与“恢复”机制,将传统的同步代码拆解为一系列可以被暂停的子程序,极大提升了异步编程的可读性与性能。本文将从语法、实现原理、典型应用以及性能优化四个角度,系统阐述C++17协程的实现细节与实战经验。
1. 协程基本概念
协程是一种用户级别的轻量级线程,允许函数在执行过程中多次挂起(co_await)并恢复(co_return)。与传统回调相比,协程可以像同步代码一样书写异步逻辑,避免回调地狱。
关键术语:
- 悬挂点:
co_await/co_yield/co_return的执行点。 - 协程句柄:
std::coroutine_handle,用于手动管理协程生命周期。 - 协程Promise:提供协程状态、返回值以及异常处理。
2. 典型协程实现(C++17)
C++17中并没有官方协程库,但可通过std::experimental::coroutine提供的基础设施实现。下面给出一个简易异步 I/O 协程示例,演示如何在 Windows 上结合 Winsock 实现非阻塞读写。
#include <experimental/coroutine>
#include <winsock2.h>
#include <ws2tcpip.h>
#include <iostream>
#include <thread>
#include <chrono>
using namespace std::experimental;
// 简易协程返回值包装
template<typename T>
struct Awaitable {
struct promise_type {
T value_;
std::exception_ptr eptr_;
auto get_return_object() {
return Awaitable{std::experimental::coroutine_handle <promise_type>::from_promise(*this)};
}
std::suspend_never initial_suspend() { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
void unhandled_exception() { eptr_ = std::current_exception(); }
void return_value(T v) { value_ = std::move(v); }
};
std::experimental::coroutine_handle <promise_type> h_;
T result() { return h_.promise().value_; }
void resume() { h_.resume(); }
};
// 异步接收
Awaitable <int> async_recv(SOCKET sock, char* buf, int len) {
// 让 Winsock 以非阻塞模式工作
int flags = 0;
int recvLen = ::recv(sock, buf, len, flags);
if (recvLen == SOCKET_ERROR) {
if (WSAGetLastError() != WSAEWOULDBLOCK) throw std::runtime_error("recv failed");
co_await std::experimental::suspend_always{}; // 这里演示挂起
recvLen = ::recv(sock, buf, len, flags); // 再次尝试
}
co_return recvLen;
}
// 主协程
Awaitable <void> main_co(SOCKET sock) {
char buffer[1024];
int n = co_await async_recv(sock, buffer, sizeof(buffer));
std::cout << "收到 " << n << " 字节: " << std::string(buffer, n) << std::endl;
co_return;
}
int main() {
// 初始化 Winsock
WSADATA wsa;
WSAStartup(MAKEWORD(2,2), &wsa);
// 创建 TCP 连接(省略错误检查)
SOCKET sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
// 设为非阻塞
u_long mode = 1;
ioctlsocket(sock, FIONBIO, &mode);
// 连接远程服务器
sockaddr_in addr{};
addr.sin_family = AF_INET;
addr.sin_port = htons(80);
inet_pton(AF_INET, "example.com", &addr.sin_addr);
connect(sock, (sockaddr*)&addr, sizeof(addr));
// 启动协程
auto co = main_co(sock);
co.resume(); // 触发协程执行
// 简单等待(生产者-消费者示例)
std::this_thread::sleep_for(std::chrono::seconds(1));
closesocket(sock);
WSACleanup();
}
说明:此代码示例仅演示协程与非阻塞 I/O 的基本交互。真实项目中需实现事件循环、任务调度与错误重试机制。
3. 协程与事件循环
在高性能网络库(如 libuv、Boost.Asio)中,协程往往与事件循环紧密耦合。常见做法:
- 事件循环:
io_context或自定义事件表。 - 协程调度:当协程挂起时,事件循环等待对应事件(I/O、定时器等)触发后继续。
- 协程池:为减少栈分配与上下文切换,可采用协程池机制。
4. 性能与优化
- 栈大小:默认协程栈为 8KB,足以处理大多数业务;若需更大栈,可通过
std::experimental::coroutine_handle::promise().initialize()自定义。 - 避免频繁挂起:每次挂起/恢复会产生上下文切换,尽量将协程拆分为较大逻辑块。
- 协程池:重用
std::coroutine_handle,减少堆内存分配。 - 异常传播:通过
promise_type::unhandled_exception把异常传播到调用层,避免隐藏错误。
5. 小结
C++17 协程(实验性)为异步编程提供了更接近同步代码的写法。虽然官方标准尚未完全规范,但通过 std::experimental::coroutine 已可实现高效、可读性强的异步逻辑。随着 C++20 的正式发布,协程将得到更完善的支持与生态,值得开发者提前了解与实践。