**C++20 中的协程(Coroutines)——如何使用与调试**

协程(Coroutines)是 C++20 标准引入的重要特性,它为异步编程、生成器、协作式多任务等提供了语法层面的支持。相比传统的基于回调或线程的异步模型,协程在代码可读性、错误处理以及性能上都有显著优势。本文将从基本概念、语法实现、常见错误以及调试技巧四个方面,帮助你快速上手 C++20 协程。


1. 协程的基本概念

1.1 什么是协程?

协程是一种可以挂起(suspend)和恢复(resume)执行的函数。它们与传统函数不同之处在于:协程可以在执行过程中暂停,随后从暂停点继续,而不是像普通函数那样从头到尾一次性完成。

1.2 为什么需要协程?

  • 异步编程:协程让异步代码写起来像同步代码,消除了回调地狱。
  • 生成器:使用协程可以轻松实现惰性序列生成。
  • 协作式多任务:在单线程内实现多任务切换,减少线程切换开销。

2. 语法实现

C++20 协程通过 co_awaitco_yieldco_return 等关键字实现挂起与恢复,并依赖于 协程类型std::futuregenerator 等)来控制协程的生命周期。

2.1 基础结构

#include <coroutine>
#include <iostream>

struct coro_handle;

struct generator {
    struct promise_type {
        int current_value;
        std::suspend_always yield_value(int v) {
            current_value = v;
            return {};
        }
        std::suspend_always initial_suspend() { return {}; }
        std::suspend_always final_suspend() noexcept { return {}; }
        generator get_return_object() {
            return {std::coroutine_handle <promise_type>::from_promise(*this)};
        }
        void return_void() {}
        void unhandled_exception() { std::terminate(); }
    };

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

    struct iterator {
        std::coroutine_handle <promise_type> coro;
        iterator(std::coroutine_handle <promise_type> h) : coro(h) {}
        int operator*() const { return coro.promise().current_value; }
        iterator& operator++() {
            coro.resume();
            return *this;
        }
        bool operator!=(const iterator& other) const { return coro != other.coro; }
    };

    iterator begin() { coro.resume(); return iterator{coro}; }
    iterator end()   { return iterator{nullptr}; }
};

2.2 生成器示例

generator fib(int n) {
    int a = 0, b = 1;
    for (int i = 0; i < n; ++i) {
        co_yield a;
        int next = a + b;
        a = b;
        b = next;
    }
}

2.3 异步任务示例

#include <future>

std::future <int> async_add(int a, int b) {
    co_return a + b;
}

3. 常见错误与调试技巧

3.1 co_await 只能在可 await 的类型上使用

int main() {
    int x = 5;
    co_await x; // ❌ compile error
}

解决方案:使用 std::suspend_alwaysstd::suspend_never 包装对象。

3.2 协程函数返回类型不匹配

generator foo() {
    co_return 42; // ❌ error: cannot return int from generator
}

解决方案co_return 必须与 promise_type 的返回类型匹配。

3.3 未处理的异常导致程序终止

协程内部抛出的异常如果未在 unhandled_exception() 中处理,将触发 std::terminate()

解决方案:在 promise_type::unhandled_exception() 中使用 std::current_exception() 或记录日志。

3.4 调试协程

  • 使用 GDB 或 LLDBbt 可以查看堆栈,但因为协程拆分函数,可能看不到完整的调用链。使用 info coroutine(GDB)可以列出所有协程实例。
  • 自定义 promise_type:在 yield_valueinitial_suspendfinal_suspend 中加入 std::cout 打印调试信息。
  • 可视化工具:Clang 自带的 -fcoroutines 选项可生成中间文件,配合 llvm-cov 观察协程执行路径。

4. 性能考量

  • 堆栈使用:协程的协程帧在堆上分配,避免了线程堆栈的限制,但需要注意堆栈大小与对象数量。
  • 上下文切换开销:协程切换比线程切换更轻量,但过多的挂起/恢复会带来函数调用开销。
  • 异步 I/O:与事件循环配合使用可显著降低系统资源占用。

5. 小结

  • C++20 协程通过 co_await/co_yield/co_return 让异步、生成器代码写起来像同步代码。
  • 协程类型(promise_type)决定了协程的生命周期、挂起行为和返回值。
  • 常见错误主要来自类型不匹配、异常未处理以及 co_await 使用错误。
  • 调试时可以利用 GDB 的 coroutine 相关命令,或在 promise_type 中加入日志。
  • 性能方面协程比线程更轻量,但需注意堆栈分配和上下文切换频率。

通过掌握上述基本概念与技巧,你就能在项目中自如地使用 C++20 协程,实现高效、可维护的异步逻辑。祝编码愉快!

发表评论