C++17协程:实现异步编程的全新方式

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)中,协程往往与事件循环紧密耦合。常见做法:

  1. 事件循环io_context 或自定义事件表。
  2. 协程调度:当协程挂起时,事件循环等待对应事件(I/O、定时器等)触发后继续。
  3. 协程池:为减少栈分配与上下文切换,可采用协程池机制。

4. 性能与优化

  • 栈大小:默认协程栈为 8KB,足以处理大多数业务;若需更大栈,可通过 std::experimental::coroutine_handle::promise().initialize() 自定义。
  • 避免频繁挂起:每次挂起/恢复会产生上下文切换,尽量将协程拆分为较大逻辑块。
  • 协程池:重用 std::coroutine_handle,减少堆内存分配。
  • 异常传播:通过 promise_type::unhandled_exception 把异常传播到调用层,避免隐藏错误。

5. 小结

C++17 协程(实验性)为异步编程提供了更接近同步代码的写法。虽然官方标准尚未完全规范,但通过 std::experimental::coroutine 已可实现高效、可读性强的异步逻辑。随着 C++20 的正式发布,协程将得到更完善的支持与生态,值得开发者提前了解与实践。

发表评论