欢迎光临
我们一直在努力

C++20协程深度指南:从入门到生产级并发编程实战

C++20 标准引入了协程(Coroutines),这是 C++ 语言历史上最重要的特性之一。协程提供了一种全新的异步编程范式,使得开发者可以用同步风格编写异步代码,极大地简化了回调地狱和复杂状态机的问题。本文将深入探讨 C++20 协程的核心概念、标准库支持、高级用法以及生产环境中的最佳实践。

一、什么是协程?

协程是一种可以挂起(suspend)和恢复(resume)的函数。与普通函数不同,协程在执行过程中可以暂停执行,保存当前状态,然后返回到调用者;之后可以在适当的时机从暂停点继续执行。

C++20 的协程是”无栈协程”(stackless coroutines),这意味着它们不拥有独立的调用栈。每个协程在堆上分配一个协程帧(coroutine frame)来保存挂起时的局部变量和状态,而不是像线程那样占用完整的栈空间。这使得 C++20 协程的开销极低——创建一个协程的成本与进行一次小内存分配相当。

特性 线程 C++20 协程
调度方式 抢占式 协作式
栈空间 有栈(1-8MB) 无栈(协程帧,通常几百字节)
创建开销 高(系统调用) 极低(一次堆分配)
上下文切换 内核态切换 用户态函数调用
并发数量 数千 数十万到百万
标准支持 C++11 起 C++20 起

协程与线程并非互斥关系,而是互补关系。协程通常运行在线程之上,利用线程池来调度协程的执行。一个典型的架构是”I/O 线程池 + 协程”,即由少量线程驱动大量协程的执行。

二、C++20 协程的核心概念

C++20 协程的实现依赖于几个关键概念:promise 对象、awaitable 对象、协程句柄(coroutine handle)以及 co_await、co_yield、co_return 三个关键字。

2.1 三个新关键字

  • co_await:挂起当前协程,等待某个操作完成
  • co_yield:返回一个值给调用者并挂起协程
  • co_return:结束协程并返回最终值

如果一个函数体中含有上述任何一个关键字,则该函数自动成为协程。

2.2 Promise 对象

每个协程都关联一个 Promise 对象,它定义协程的行为:如何创建协程结果、如何处理异常、以及协程挂起/恢复时的行为。Promise 类型通过协程返回类型的 promise_type 成员类型指定。

struct ReturnType {
    struct promise_type {
        ReturnType get_return_object() { return {}; }
        std::suspend_never initial_suspend() { return {}; }
        std::suspend_never final_suspend() noexcept { return {}; }
        void return_void() {}
        void unhandled_exception() { std::terminate(); }
    };
};

2.3 协程句柄(Coroutine Handle)

协程句柄是操作协程的”遥控器”。通过它,我们可以恢复协程、销毁协程、访问协程的promise对象。std::coroutine_handle<> 是一个类型擦除的句柄,而 std::coroutine_handle<PromiseType> 则知道具体的 promise 类型。

2.4 Awaitable 对象

awaitable 是可以在 co_await 表达式中使用的对象。标准库提供了两个基本的 awaiter:

  • std::suspend_never:告诉协程永远不要挂起(直接继续执行)
  • std::suspend_always:告诉协程总是挂起

自定义 awaiter 需要实现三个方法:

struct MyAwaiter {
    // 是否挂起?返回 true 则挂起,false 则继续
    bool await_ready() const noexcept { return false; }

    // 挂起后调用,返回 void 或 coroutine_handle
    void await_suspend(std::coroutine_handle<> handle) {
        // 保存 handle,稍后恢复
    }

    // 恢复时调用,返回值即为 co_await 表达式的结果
    int await_resume() noexcept { return 42; }
};

三、实战:构建一个异步网络库

理论知识足够后,让我们用协程实现一个实际可用的异步 TCP 客户端。这个例子将展示协程如何让异步代码变得简洁。

3.1 封装 Task 类型

#include <coroutine>
#include <exception>
#include <functional>
#include <memory>
#include <vector>

template <typename T>
struct Task {
    struct promise_type {
        T result_;
        std::exception_ptr exception_;

        Task get_return_object() {
            return Task{
                std::coroutine_handle<promise_type>::from_promise(*this)
            };
        }
        std::suspend_never initial_suspend() { return {}; }
        std::suspend_always final_suspend() noexcept { return {}; }

        void return_value(T value) {
            result_ = std::move(value);
        }

        void unhandled_exception() {
            exception_ = std::current_exception();
        }
    };

    std::coroutine_handle<promise_type> handle_;

    explicit Task(std::coroutine_handle<promise_type> h) : handle_(h) {}
    ~Task() { if (handle_) handle_.destroy(); }

    Task(Task&& other) noexcept
        : handle_(std::exchange(other.handle_, {})) {}

    T get_result() {
        if (handle_.promise().exception_) {
            std::rethrow_exception(handle_.promise().exception_);
        }
        return handle_.promise().result_;
    }
};

3.2 事件循环

#include <sys/epoll.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <fcntl.h>
#include <unistd.h>
#include <map>
#include <functional>

class EventLoop {
    int epoll_fd_;
    std::map<int, std::coroutine_handle<>> pending_;

public:
    EventLoop() {
        epoll_fd_ = epoll_create1(0);
    }

    void await_readable(int fd, std::coroutine_handle<> handle) {
        struct epoll_event ev;
        ev.events = EPOLLIN | EPOLLONESHOT;
        ev.data.fd = fd;
        epoll_ctl(epoll_fd_, EPOLL_CTL_ADD, fd, &ev);
        pending_[fd] = handle;
    }

    void run() {
        while (!pending_.empty()) {
            struct epoll_event events[64];
            int n = epoll_wait(epoll_fd_, events, 64, -1);
            for (int i = 0; i < n; ++i) {
                int fd = events[i].data.fd;
                auto handle = pending_[fd];
                pending_.erase(fd);
                epoll_ctl(epoll_fd_, EPOLL_CTL_DEL, fd, nullptr);
                if (handle) handle.resume();
            }
        }
    }

    ~EventLoop() { close(epoll_fd_); }
};

// 全局事件循环
EventLoop& get_loop() {
    static EventLoop loop;
    return loop;
}

3.3 异步 Awaiter

struct ReadableAwaiter {
    int fd_;
    bool await_ready() { return false; }
    void await_suspend(std::coroutine_handle<> h) {
        get_loop().await_readable(fd_, h);
    }
    void await_resume() {}
};

struct ConnectAwaiter {
    int fd_;
    sockaddr_in addr_;

    ConnectAwaiter(int fd, sockaddr_in addr) : fd_(fd), addr_(addr) {}

    bool await_ready() { return false; }

    void await_suspend(std::coroutine_handle<> h) {
        // 非阻塞 connect
        int rc = connect(fd_, (struct sockaddr*)&addr_, sizeof(addr_));
        if (rc == 0) {
            h.resume(); // 立即连接成功
            return;
        }
        if (errno == EINPROGRESS) {
            // 连接进行中,等待可写事件
            struct epoll_event ev;
            ev.events = EPOLLOUT | EPOLLONESHOT;
            ev.data.fd = fd_;
            epoll_ctl(get_loop().get_fd(), EPOLL_CTL_ADD, fd_, &ev);
            get_loop().add_pending_inprogress(fd_, h);
            return;
        }
        throw std::system_error(errno, std::system_category());
    }

    void await_resume() {
        int err;
        socklen_t len = sizeof(err);
        getsockopt(fd_, SOL_SOCKET, SO_ERROR, &err, &len);
        if (err != 0) throw std::system_error(err, std::system_category());
    }
};

3.4 异步 TCP 客户端

#include <string>
#include <vector>

Task<std::string> async_http_get(const std::string& host,
                                  const std::string& path) {
    int sock = socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK, 0);
    if (sock < 0) throw std::system_error(errno, std::system_category());

    struct sockaddr_in addr{};
    addr.sin_family = AF_INET;
    addr.sin_port = htons(80);
    inet_pton(AF_INET, host.c_str(), &addr.sin_addr);

    co_await ConnectAwaiter{sock, addr};

    std::string request =
        "GET " + path + " HTTP/1.1\r\n"
        "Host: " + host + "\r\n"
        "Connection: close\r\n\r\n";

    ::send(sock, request.data(), request.size(), 0);

    std::string response;
    char buf[4096];
    while (true) {
        co_await ReadableAwaiter{sock};
        ssize_t n = ::read(sock, buf, sizeof(buf));
        if (n <= 0) break;
        response.append(buf, n);
    }

    close(sock);
    co_return response;
}

四、高级模式:Generator 与 Lazy Evaluation

除了异步 I/O,协程的另一个重要应用场景是生成器(Generator)。co_yield 关键字使得创建惰性求值的数列变得非常简单。

#include <coroutine>
#include <optional>

template <typename T>
struct Generator {
    struct promise_type {
        T current_value_;

        Generator get_return_object() {
            return Generator{
                std::coroutine_handle<promise_type>::from_promise(*this)
            };
        }
        std::suspend_always initial_suspend() { return {}; }
        std::suspend_always final_suspend() noexcept { return {}; }
        void return_void() {}

        std::suspend_always yield_value(T value) {
            current_value_ = std::move(value);
            return {};
        }
        void unhandled_exception() { std::terminate(); }
    };

    std::coroutine_handle<promise_type> handle_;

    Generator(auto h) : handle_(h) {}
    ~Generator() { if (handle_) handle_.destroy(); }

    Generator(Generator&& other) noexcept
        : handle_(std::exchange(other.handle_, {})) {}

    bool next() {
        if (!handle_ || handle_.done()) return false;
        handle_.resume();
        return !handle_.done();
    }

    T value() { return handle_.promise().current_value_; }
};

// 使用生成器生成斐波那契数列
Generator<size_t> fibonacci() {
    size_t a = 0, b = 1;
    while (true) {
        co_yield a;
        auto next = a + b;
        a = b;
        b = next;
    }
}

// 使用示例
void print_fibonacci(int n) {
    auto fib = fibonacci();
    for (int i = 0; i < n && fib.next(); ++i) {
        std::cout << fib.value() << " ";
    }
    // 输出: 0 1 1 2 3 5 8 13 21 34
}

五、C++23 对协程的增强

C++23 在协程生态方面做了重要的改进,主要包括 std::generator 标准生成器和 std::print 格式化输出支持。

5.1 std::generator(C++23)

C++23 标准库引入了 std::generator<T>,使得编写生成器不再需要手动实现 promise_type:

#include <generator>
#include <ranges>

std::generator<int> squares(int n) {
    for (int i = 0; i < n; ++i) {
        co_yield i * i;
    }
}

// 直接用于 range-based for 循环
for (int x : squares(10)) {
    std::print("{} ", x);
}
// 输出: 0 1 4 9 16 25 36 49 64 81

5.2 与 Ranges 库的集成

std::generator 实现了 range 接口,可以直接与标准库中的 range adapter 配合使用:

#include <generator>
#include <ranges>
#include <algorithm>

std::generator<int> range(int start, int end, int step = 1) {
    for (int i = start; i < end; i += step) {
        co_yield i;
    }
}

int main() {
    auto values = range(0, 100)
        | std::views::filter([](int x) { return x % 2 == 0; })
        | std::views::transform([](int x) { return x * 3; })
        | std::views::take(10);

    for (int v : values) {
        std::print("{} ", v);
    }
    // 输出: 0 6 12 18 24 30 36 42 48 54
}

六、生产环境最佳实践

在生产环境中使用 C++20 协程时,有几个关键点需要特别注意。

6.1 性能优化

  • 避免不必要的堆分配:C++20 标准允许编译器在某些情况下省略协程帧的堆分配(通过 HALO 优化)。将协程定义为 inline 或在同一翻译单元中定义调用方和被调用方可以提高优化机会。
  • 使用 custom allocator:为协程帧提供自定义分配器可以显著减少内存分配开销,特别是在高并发场景下。
  • 注意 initial_suspend:如果协程在初始挂起后立即需要被恢复,使用 std::suspend_never 可以避免一次不必要的挂起-恢复开销。
  • 小心 final_suspendfinal_suspend 必须是 noexcept,且如果使用 std::suspend_never,协程框架会在协程完成后自动销毁。使用 std::suspend_always 则需要在外部显式销毁。

6.2 内存安全与生命周期

  • 悬垂引用问题:协程挂起后,函数中的局部变量会被保存在协程帧中。但如果协程帧被销毁(例如协程句柄被提前释放),则保留的引用和指针都会失效。
  • 协程句柄所有权:明确谁拥有 std::coroutine_handle 的所有权。常见模式是将协程句柄包裹在智能指针或 RAII 包装器中。
  • 异常安全:Promise 的 unhandled_exception 方法负责处理逃逸出协程体的异常。一定要将异常存储起来,在获取结果时重新抛出。

6.3 调试技巧

  • 启用 AddressSanitizer:编译器参数 -fsanitize=address 可以帮助检测协程相关的内存错误。
  • 使用 libc++ 的调试支持:Clang 的 libc++ 提供了对 std::coroutine_handle 的调试断言。
  • 日志跟踪:在 await_suspendawait_resume 中添加日志,可以清晰看到协程的执行流程。
  • GDB 支持:GDB 10+ 开始支持打印 std::coroutine_handle 的状态,可以查看协程是否完成。

6.4 与第三方库的集成

许多现代 C++ 库已经提供了对 C++20 协程的支持:

库名称 协程支持 说明
Boost.Asio ✅ 完全支持 提供了 co_spawnasio::use_awaitable
libuv ✅ 通过 uvw 封装 需要自定义 awaiter 封装
cppcoro ✅ 原生协程库 提供了 Task、Generator、AsyncGenerator 等
folly ✅ 完全支持 Facebook 的 folly::coro 命名空间
libco ❌ 不兼容 腾讯的协程库,基于有栈协程

七、协程 vs 传统异步模式

让我们用一个实际场景来对比协程和其他异步编程方式的差异:同时发送多个 HTTP 请求并聚合结果。

传统回调方式(假代码):

void fetch_all(std::vector<std::string> urls, Callback callback) {
    auto results = std::make_shared<std::vector<std::string>>(urls.size());
    auto counter = std::make_shared<int>(0);

    for (size_t i = 0; i < urls.size(); ++i) {
        http_get_async(urls[i], [i, results, counter, callback](std::string r) {
            (*results)[i] = r;
            if (++(*counter) == results->size()) {
                callback(*results);
            }
        });
    }
}
// 错误处理?几乎不可能优雅实现

协程方式:

Task<std::vector<std::string>> fetch_all(std::vector<std::string> urls) {
    std::vector<std::string> results;
    results.reserve(urls.size());

    for (const auto& url : urls) {
        // 这里可以并发启动多个协程
        results.push_back(co_await async_http_get(url));
    }

    co_return results;
}

Task<std::vector<std::string>> fetch_all_parallel(
        std::vector<std::string> urls) {
    std::vector<Task<std::string>> tasks;
    for (const auto& url : urls) {
        tasks.push_back(async_http_get(url));
    }
    std::vector<std::string> results;
    for (auto& t : tasks) {
        results.push_back(t.get_result());
    }
    co_return results;
}

协程版本的代码量减半,逻辑清晰,错误处理自然(使用 try-catch),而且可以轻松从串行切换为并行。

八、总结与展望

C++20 协程为 C++ 带来了真正现代的异步编程能力。虽然学习曲线相对陡峭——需要理解 promise_type、awaitable、coroutine_handle 等概念——但一旦掌握,它能极大提升异步代码的可读性和可维护性。

目前 C++20 协程的主要痛点在于:

  • 标准库支持仍然有限(C++23 才加入 std::generator
  • 没有标准的 async Task 类型(需要第三方库或自行实现)
  • 异常处理的 boilerplate 较多
  • 编译器支持仍不完全一致(GCC、Clang、MSVC 各有差异)

展望 C++26,预计会进一步改进协程的支持:引入 std::async_scope 作为标准异步作用域,更好支持结构化并发的 fire-and-forget 和结构化任务。协程在 C++ 中的位置将越来越重要,成为高性能服务器开发、游戏引擎、实时系统等领域的标配技术。

C++ programming code on screen

对于团队来说,建议从非关键路径开始尝试协程,比如日志记录、指标收集、配置文件加载等辅助功能。随着经验积累,再逐步将其推广到核心业务逻辑中。配合现代 C++ 的其他特性(如 Concepts、Ranges、Modules),C++20/23 正在将 C++ 带入一个新的黄金时代。

【本站文章皆为原创,未经允许不得转载】:汤不热吧 » C++20协程深度指南:从入门到生产级并发编程实战
分享到: 更多 (0)