深入学习CPP20_协程

深入学习 C++20 协程(Coroutines)

头文件:<coroutine>

命名空间:std

编译器要求:GCC 11+ / Clang 14+ / MSVC 19.28+(均需 -std=c++20 或以上)

注意:GCC 10 / Clang 8~13 可通过 -fcoroutines<experimental/coroutine> 使用实验性支持


一、为什么需要协程?

1.1 异步编程的传统痛点

游戏服务器中充斥着异步操作------数据库查询、网络 I/O、定时器回调。传统方案各有各的痛:

方案 A:回调地狱(Callback Hell)

cpp 复制代码
void HandleLogin(Connection* conn, const LoginPacket& pkt) {
    // 第1步:查询数据库验证账号
    dbManager->QueryAsync("SELECT * FROM accounts WHERE name=?", pkt.name,
        [conn, pkt](const DBResult& result) {
            if (!result.ok) { conn->SendError("DB错误"); return; }
            // 第2步:查询角色列表
            dbManager->QueryAsync("SELECT * FROM characters WHERE account_id=?", result.accountId,
                [conn](const DBResult& charResult) {
                    if (!charResult.ok) { conn->SendError("DB错误"); return; }
                    // 第3步:加载角色数据
                    dbManager->QueryAsync("SELECT * FROM inventory WHERE char_id=?", charResult.charId,
                        [conn, charResult](const DBResult& invResult) {
                            // 第4步:终于可以发送登录成功了...
                            conn->SendLoginSuccess(charResult, invResult);
                        });
                });
        });
}

方案 B:状态机(State Machine)

cpp 复制代码
class LoginHandler {
    enum State { INIT, WAITING_ACCOUNT, WAITING_CHARS, WAITING_INVENTORY, DONE };
    State state_ = INIT;
    // 每个阶段的中间数据都要存为成员变量
    int accountId_;
    int charId_;
    DBResult charResult_;

public:
    void OnDBResult(const DBResult& result) {
        switch (state_) {
            case WAITING_ACCOUNT:
                accountId_ = result.accountId;
                QueryCharacters(accountId_);
                state_ = WAITING_CHARS;
                break;
            case WAITING_CHARS:
                charId_ = result.charId;
                charResult_ = result;
                QueryInventory(charId_);
                state_ = WAITING_INVENTORY;
                break;
            case WAITING_INVENTORY:
                SendLoginSuccess(charResult_, result);
                state_ = DONE;
                break;
        }
    }
};

核心问题:

痛点 回调方案 状态机方案
代码可读性 嵌套深、逻辑碎片化 流程分散在 switch 各分支
错误处理 每层回调都要写错误处理 需要在每个状态处理异常
局部变量生命周期 需要 capture 或提升为成员 所有中间状态都要存为成员变量
调试难度 调用栈看不到完整流程 状态转换逻辑难以追踪
组合性 回调难以组合和复用 状态机难以嵌套

1.2 协程的解法:用同步的写法做异步的事

C++20 协程让你写出看起来同步、实际异步的代码:

cpp 复制代码
Task<void> HandleLogin(Connection* conn, const LoginPacket& pkt) {
    // 第1步:查询账号------挂起,等 DB 返回后恢复
    auto account = co_await dbManager->QueryAsync("SELECT * FROM accounts WHERE name=?", pkt.name);
    if (!account.ok) { conn->SendError("DB错误"); co_return; }

    // 第2步:查询角色列表------再次挂起
    auto chars = co_await dbManager->QueryAsync("SELECT * FROM characters WHERE account_id=?", account.id);
    if (!chars.ok) { conn->SendError("DB错误"); co_return; }

    // 第3步:加载背包------再次挂起
    auto inventory = co_await dbManager->QueryAsync("SELECT * FROM inventory WHERE char_id=?", chars.charId);

    // 第4步:全部完成,发送响应
    conn->SendLoginSuccess(chars, inventory);
}

一句话总结:协程把"异步等待"从回调/状态机的控制流反转,恢复为线性的顺序代码,编译器帮你管理挂起/恢复的状态保存。


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

2.1 什么是无栈协程

C++20 的协程是无栈协程(Stackless Coroutine) ------协程的状态(局部变量、挂起点)保存在堆上的协程帧中,而非像有栈协程那样拥有独立的调用栈。

有栈 vs 无栈对比:

复制代码
有栈协程(如 Boost.Context、ucontext):
┌─────────────────┐    ┌─────────────────┐
│  协程 A 的完整栈  │    │  协程 B 的完整栈  │
│  (通常 64KB~1MB) │    │  (通常 64KB~1MB) │
│  ┌─────────────┐│    │  ┌─────────────┐│
│  │ 栈帧 3      ││    │  │ 栈帧 2      ││
│  │ 栈帧 2      ││    │  │ 栈帧 1      ││
│  │ 栈帧 1      ││    │  │             ││
│  └─────────────┘│    │  └─────────────┘│
└─────────────────┘    └─────────────────┘
切换方式:保存/恢复整个 CPU 寄存器 + 栈指针

无栈协程(C++20):
┌────────────────┐    ┌────────────────┐
│ 协程帧 A (堆上) │    │ 协程帧 B (堆上) │
│ - 局部变量      │    │ - 局部变量      │
│ - 挂起点索引    │    │ - 挂起点索引    │
│ - promise 对象  │    │ - promise 对象  │
│ (按需分配大小)  │    │ (按需分配大小)  │
└────────────────┘    └────────────────┘
切换方式:普通函数调用/返回(通过 coroutine_handle::resume())

无栈协程的优势:

  • 协程帧大小按需分配(几十到几百字节),而非固定几百 KB 的栈
  • 数万个协程的内存开销可控
  • 编译器可以优化掉协程帧分配(HALO 优化)
  • 不需要平台相关的汇编/上下文切换代码

2.2 三大关键字

C++20 引入了三个关键字,函数体内出现任意一个,该函数就成为协程:

关键字 语义 典型用途
co_await 挂起协程,等待异步操作完成 异步 I/O、定时等待
co_yield 挂起协程并产出一个值 生成器、数据流
co_return 结束协程并(可选)返回值 协程正常结束
cpp 复制代码
// co_await: 异步等待
Task<int> AsyncAdd(int a, int b) {
    co_await SomeAsyncWork();
    co_return a + b;
}

// co_yield: 生成器
Generator<int> Range(int start, int end) {
    for (int i = start; i < end; ++i) {
        co_yield i;  // 产出一个值后挂起
    }
}

// co_return: 返回结果
Task<std::string> GetName() {
    co_return "Hello, Coroutine!";
}

2.3 协程的核心组件

C++20 协程由四个核心角色协作:

复制代码
        调用者(Caller)
           │
           │ 调用协程函数
           ▼
    ┌──────────────────┐
    │  协程返回类型      │ ← 如 Task<T>、Generator<T>
    │  (Return Object)  │    调用者通过它与协程交互
    └────────┬─────────┘
             │ 内部关联
    ┌────────▼─────────┐
    │  promise_type     │ ← 协程的"控制面板"
    │  (Promise 对象)   │    控制协程的生命周期和值传递
    └────────┬─────────┘
             │ 通过 coroutine_handle 关联
    ┌────────▼─────────┐
    │  协程帧            │ ← 编译器生成的状态存储
    │  (Coroutine Frame)│    包含局部变量、挂起点、promise
    └──────────────────┘

    ┌──────────────────┐
    │  Awaiter 对象      │ ← co_await 的操作数
    │  (Awaitable)      │    决定是否挂起、如何恢复
    └──────────────────┘

三、编译器变换模型

理解协程最关键的一步是理解编译器对协程函数做了什么变换

3.1 原始协程代码

cpp 复制代码
Task<int> Compute(int x) {
    int a = co_await AsyncGetValue();
    int b = co_await AsyncGetValue();
    co_return a + b + x;
}

3.2 编译器变换后的伪代码

cpp 复制代码
Task<int> Compute(int x) {
    // ① 分配协程帧(堆上)
    auto* frame = new __coroutine_frame_Compute;
    frame->x = x;  // 拷贝参数到帧中

    // ② 在帧内构造 promise 对象
    auto& promise = frame->promise;

    // ③ 获取返回对象(返回给调用者)
    Task<int> returnObject = promise.get_return_object();

    // ④ 初始挂起点
    co_await promise.initial_suspend();  // 通常返回 suspend_always 或 suspend_never

    try {
        // ⑤ 原始函数体(每个 co_await 是一个挂起/恢复点)
        // --- 挂起点 1 ---
        int a = co_await AsyncGetValue();
        // --- 挂起点 2 ---
        int b = co_await AsyncGetValue();

        // ⑥ co_return → 调用 promise.return_value(a + b + x)
        promise.return_value(a + b + x);
    } catch (...) {
        // ⑦ 异常处理
        promise.unhandled_exception();
    }

    // ⑧ 最终挂起点
    co_await promise.final_suspend();

    // ⑨ 如果没在 final_suspend 挂起,销毁协程帧
    // delete frame;

    return returnObject;  // 实际上在 ③ 之后就已经返回了
}

3.3 挂起/恢复的本质

每个 co_await 点,编译器做的事情:

复制代码
co_await expr;

展开为:
┌─────────────────────────────────────────────────────┐
│ 1. 获取 Awaiter 对象                                  │
│    auto&& awaiter = get_awaiter(expr);               │
│                                                     │
│ 2. 检查是否需要挂起                                    │
│    if (!awaiter.await_ready()) {                     │
│        // 3. 保存当前状态到协程帧                      │
│        //    (局部变量、挂起点索引)                   │
│        // 4. 调用 await_suspend                       │
│        awaiter.await_suspend(coroutine_handle);      │
│        // 5. 控制权返回给调用者/恢复者                  │
│        return;  // ← 这就是"挂起"                     │
│    }                                                 │
│ 6. 从 await_resume 获取结果                            │
│    auto result = awaiter.await_resume();             │
└─────────────────────────────────────────────────────┘

恢复时 (某处调用了 handle.resume()),执行流直接跳到上次挂起点之后,从 await_resume() 获取结果,继续执行。


四、Promise Type --- 协程的控制面板

4.1 Promise 协议全貌

promise_type 是协程最核心的定制点。编译器通过协程的返回类型找到 promise_type

cpp 复制代码
// 编译器查找规则:
// 协程返回类型为 Task<int> → 查找 Task<int>::promise_type
// 也可以通过 std::coroutine_traits 特化来指定
cpp 复制代码
struct MyPromise {
    // ═══ 必须实现 ═══

    // 构造返回对象(调用者拿到的东西)
    ReturnType get_return_object();

    // 初始挂起:suspend_always = 惰性启动,suspend_never = 立即执行
    std::suspend_always initial_suspend() noexcept;

    // 最终挂起:suspend_always = 调用者负责销毁,suspend_never = 自动销毁
    std::suspend_always final_suspend() noexcept;

    // 未捕获异常的处理
    void unhandled_exception();

    // 二选一:
    void return_void();             // 用于 co_return; 或协程体结束
    void return_value(T value);     // 用于 co_return expr;

    // ═══ 可选实现 ═══

    // 自定义 co_yield 行为
    auto yield_value(T value);

    // 自定义 co_await 转换
    auto await_transform(T expr);

    // 自定义协程帧分配
    static void* operator new(size_t size);
    static void operator delete(void* ptr, size_t size);

    // 优化:如果返回 true,跳过堆分配(提示编译器可以 HALO 优化)
    static auto get_return_object_on_allocation_failure();
};

4.2 initial_suspend 的选择

cpp 复制代码
// 惰性启动(Lazy)------ 创建后不立即执行,等调用者 resume
std::suspend_always initial_suspend() noexcept { return {}; }

// 立即启动(Eager)------ 创建后立即执行到第一个 co_await
std::suspend_never initial_suspend() noexcept { return {}; }

游戏服务器中的选择:

  • Task(异步任务) :通常用 suspend_always(惰性),由调度器决定何时启动
  • Generator(生成器) :通常用 suspend_always(惰性),调用者 pull 数据时才执行
  • Fire-and-forget :用 suspend_never(立即),启动后自行运行

4.3 final_suspend 的选择

cpp 复制代码
// 挂起在最终点 ------ 协程帧不会自动销毁,调用者需要手动 destroy()
std::suspend_always final_suspend() noexcept { return {}; }

// 不挂起 ------ 协程结束后自动销毁帧,但调用者不能再访问 handle
std::suspend_never final_suspend() noexcept { return {}; }

关键规则:final_suspend() 必须是 noexcept,因为此时异常已经没地方去了。


五、Awaiter --- co_await 的底层机制

5.1 Awaiter 三件套

任何类型只要实现以下三个方法,就可以被 co_await

cpp 复制代码
struct MyAwaiter {
    // 快速路径:结果已经就绪?不需要挂起
    bool await_ready() const noexcept;

    // 挂起时调用:安排恢复工作(注册回调、投递到其他线程等)
    // 返回值有三种形式(见下文)
    void/bool/coroutine_handle<> await_suspend(std::coroutine_handle<> h);

    // 恢复后调用:返回 co_await 表达式的结果
    T await_resume();
};

5.2 await_suspend 的三种返回类型

await_suspend 的返回类型决定了挂起后的行为:

cpp 复制代码
// 形式 1:void ------ 无条件挂起
void await_suspend(std::coroutine_handle<> h) {
    // 安排某个时机调用 h.resume()
    scheduler->enqueue(h);
}

// 形式 2:bool ------ 条件挂起
bool await_suspend(std::coroutine_handle<> h) {
    // 返回 true  → 挂起(控制权回到调用者)
    // 返回 false → 不挂起(立即恢复执行,等价于 await_ready 返回 true)
    return !result_ready_.load();
}

// 形式 3:coroutine_handle<> ------ 对称转移(Symmetric Transfer)
std::coroutine_handle<> await_suspend(std::coroutine_handle<> h) {
    // 返回的 handle 会被立即 resume ------ 不经过调用栈
    // 这避免了递归 resume 导致的栈溢出
    h.destroy();
    return continuation_;  // 直接跳到等待者,不回到调用者
}

对称转移(Symmetric Transfer) 是 C++20 协程的一个关键特性:

复制代码
普通恢复(resume 嵌套):              对称转移:
Caller                              Caller
  └→ A.resume()                       └→ A.resume()
       └→ B.resume()                       A 返回 B 的 handle
            └→ C.resume()                  └→ B.resume()
                 └→ ...                        B 返回 C 的 handle
            栈深度持续增长!                    └→ C.resume()
                                               栈深度恒定 = 1

5.3 标准库提供的两个 Awaiter

cpp 复制代码
// 永远不挂起------co_await 直接跳过
struct suspend_never {
    bool await_ready() const noexcept { return true; }   // 已就绪
    void await_suspend(std::coroutine_handle<>) const noexcept {}
    void await_resume() const noexcept {}
};

// 永远挂起------co_await 一定会挂起
struct suspend_always {
    bool await_ready() const noexcept { return false; }  // 未就绪
    void await_suspend(std::coroutine_handle<>) const noexcept {}
    void await_resume() const noexcept {}
};

六、从零实现协程类型

6.1 实现 Generator --- 惰性序列生成器

cpp 复制代码
#include <coroutine>
#include <optional>
#include <exception>
#include <utility>

template <typename T>
class Generator {
public:
    struct promise_type {
        std::optional<T> currentValue;
        std::exception_ptr exception;

        Generator get_return_object() {
            return Generator{std::coroutine_handle<promise_type>::from_promise(*this)};
        }

        // 惰性启动:创建后暂停,等调用者 pull
        std::suspend_always initial_suspend() noexcept { return {}; }

        // 结束时挂起,让调用者有机会读取最后状态
        std::suspend_always final_suspend() noexcept { return {}; }

        // co_yield value → 存储值并挂起
        std::suspend_always yield_value(T value) {
            currentValue = std::move(value);
            return {};
        }

        void return_void() {}

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

private:
    std::coroutine_handle<promise_type> handle_;

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

    // 禁止拷贝
    Generator(const Generator&) = delete;
    Generator& operator=(const Generator&) = delete;

    // 支持移动
    Generator(Generator&& other) noexcept : handle_(std::exchange(other.handle_, nullptr)) {}
    Generator& operator=(Generator&& other) noexcept {
        if (this != &other) {
            if (handle_) handle_.destroy();
            handle_ = std::exchange(other.handle_, nullptr);
        }
        return *this;
    }

    // 迭代器接口------支持 range-for
    struct Iterator {
        std::coroutine_handle<promise_type> handle;

        Iterator& operator++() {
            handle.resume();
            if (handle.done()) {
                // 协程结束,检查是否有未处理异常
                if (handle.promise().exception) {
                    std::rethrow_exception(handle.promise().exception);
                }
            }
            return *this;
        }

        const T& operator*() const { return *handle.promise().currentValue; }
        bool operator==(std::default_sentinel_t) const { return handle.done(); }
    };

    Iterator begin() {
        handle_.resume();  // 推进到第一个 co_yield
        return Iterator{handle_};
    }

    std::default_sentinel_t end() { return {}; }
};

使用示例:

cpp 复制代码
// 斐波那契数列生成器------无限序列,惰性求值
Generator<uint64_t> Fibonacci() {
    uint64_t a = 0, b = 1;
    while (true) {
        co_yield a;
        auto next = a + b;
        a = b;
        b = next;
    }
}

// 使用
auto fib = Fibonacci();
int count = 0;
for (auto val : fib) {
    printf("%llu ", val);  // 0 1 1 2 3 5 8 13 21 34
    if (++count >= 10) break;
}

6.2 实现 Task --- 异步任务

cpp 复制代码
#include <coroutine>
#include <exception>
#include <optional>
#include <utility>

template <typename T>
class Task {
public:
    struct promise_type {
        std::optional<T> result;
        std::exception_ptr exception;
        std::coroutine_handle<> continuation;  // 等待此 Task 的协程

        Task get_return_object() {
            return Task{std::coroutine_handle<promise_type>::from_promise(*this)};
        }

        // 惰性启动
        std::suspend_always initial_suspend() noexcept { return {}; }

        // 最终挂起时,通过对称转移恢复等待者
        auto final_suspend() noexcept {
            struct FinalAwaiter {
                bool await_ready() const noexcept { return false; }
                // 对称转移:直接跳到等待此 Task 的协程
                std::coroutine_handle<> await_suspend(
                    std::coroutine_handle<promise_type> h) noexcept {
                    auto continuation = h.promise().continuation;
                    return continuation ? continuation : std::noop_coroutine();
                }
                void await_resume() noexcept {}
            };
            return FinalAwaiter{};
        }

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

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

    // co_await Task<T> 时使用的 Awaiter
    auto operator co_await() const noexcept {
        struct TaskAwaiter {
            std::coroutine_handle<promise_type> handle;

            bool await_ready() const noexcept {
                return handle.done();
            }

            // 当前协程挂起,记录为 continuation,然后启动目标 Task
            std::coroutine_handle<> await_suspend(std::coroutine_handle<> caller) noexcept {
                handle.promise().continuation = caller;
                return handle;  // 对称转移:立即开始执行此 Task
            }

            T await_resume() {
                if (handle.promise().exception) {
                    std::rethrow_exception(handle.promise().exception);
                }
                return std::move(*handle.promise().result);
            }
        };
        return TaskAwaiter{handle_};
    }

    // 同步等待(在非协程上下文中使用)
    T SyncWait() {
        handle_.resume();
        // 注意:这只适用于不涉及真正异步 I/O 的场景
        // 真正的异步场景需要事件循环驱动
        if (handle_.promise().exception) {
            std::rethrow_exception(handle_.promise().exception);
        }
        return std::move(*handle_.promise().result);
    }

private:
    std::coroutine_handle<promise_type> handle_;

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

    Task(const Task&) = delete;
    Task& operator=(const Task&) = delete;
    Task(Task&& other) noexcept : handle_(std::exchange(other.handle_, nullptr)) {}
    Task& operator=(Task&& other) noexcept {
        if (this != &other) {
            if (handle_) handle_.destroy();
            handle_ = std::exchange(other.handle_, nullptr);
        }
        return *this;
    }
};

// void 特化
template <>
class Task<void> {
public:
    struct promise_type {
        std::exception_ptr exception;
        std::coroutine_handle<> continuation;

        Task get_return_object() {
            return Task{std::coroutine_handle<promise_type>::from_promise(*this)};
        }

        std::suspend_always initial_suspend() noexcept { return {}; }

        auto final_suspend() noexcept {
            struct FinalAwaiter {
                bool await_ready() const noexcept { return false; }
                std::coroutine_handle<> await_suspend(
                    std::coroutine_handle<promise_type> h) noexcept {
                    auto continuation = h.promise().continuation;
                    return continuation ? continuation : std::noop_coroutine();
                }
                void await_resume() noexcept {}
            };
            return FinalAwaiter{};
        }

        void return_void() {}

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

    auto operator co_await() const noexcept {
        struct TaskAwaiter {
            std::coroutine_handle<promise_type> handle;
            bool await_ready() const noexcept { return handle.done(); }
            std::coroutine_handle<> await_suspend(std::coroutine_handle<> caller) noexcept {
                handle.promise().continuation = caller;
                return handle;
            }
            void await_resume() {
                if (handle.promise().exception) {
                    std::rethrow_exception(handle.promise().exception);
                }
            }
        };
        return TaskAwaiter{handle_};
    }

private:
    std::coroutine_handle<promise_type> handle_;

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

    Task(const Task&) = delete;
    Task& operator=(const Task&) = delete;
    Task(Task&& other) noexcept : handle_(std::exchange(other.handle_, nullptr)) {}
    Task& operator=(Task&& other) noexcept {
        if (this != &other) {
            if (handle_) handle_.destroy();
            handle_ = std::exchange(other.handle_, nullptr);
        }
        return *this;
    }
};

使用示例:

cpp 复制代码
Task<int> ComputeAsync(int x) {
    co_return x * 2;
}

Task<int> CombineAsync() {
    int a = co_await ComputeAsync(10);  // a = 20
    int b = co_await ComputeAsync(21);  // b = 42
    co_return a + b;                     // 62
}

6.3 实现自定义 Awaiter --- 异步定时器

cpp 复制代码
#include <chrono>
#include <coroutine>

// 假设存在一个定时器管理器
class TimerManager {
public:
    using Callback = std::function<void()>;
    void AddTimer(int delayMs, Callback cb);
    static TimerManager& Instance();
};

// co_await SleepFor(100ms) ------ 挂起协程,100ms 后恢复
struct SleepFor {
    std::chrono::milliseconds duration;

    bool await_ready() const noexcept {
        return duration <= std::chrono::milliseconds::zero();  // 0ms 不需要挂起
    }

    void await_suspend(std::coroutine_handle<> h) const {
        // 注册定时器,到期后恢复协程
        TimerManager::Instance().AddTimer(
            static_cast<int>(duration.count()),
            [h]() mutable { h.resume(); }
        );
    }

    void await_resume() const noexcept {}
};

// 使用
Task<void> DelayedGreeting() {
    printf("开始等待...\n");
    co_await SleepFor{std::chrono::milliseconds{1000}};
    printf("1秒后:Hello!\n");
    co_await SleepFor{std::chrono::milliseconds{2000}};
    printf("又过了2秒:World!\n");
}

七、await_transform --- 协程级别的 co_await 拦截

promise_type 可以定义 await_transform 来拦截和转换所有 co_await 表达式:

cpp 复制代码
struct ScheduledPromise {
    // 拦截所有 co_await 表达式
    template <typename T>
    auto await_transform(T&& awaitable) {
        // 可以在这里做日志、权限检查、调度等
        return std::forward<T>(awaitable);  // 直接透传
    }

    // 特殊处理:禁止在此协程中 co_await 某些类型
    auto await_transform(std::suspend_always) = delete;  // 编译错误!

    // 特殊处理:自动注入调度逻辑
    auto await_transform(SwitchToThread target) {
        struct ThreadSwitchAwaiter {
            SwitchToThread target;
            bool await_ready() const noexcept { return false; }
            void await_suspend(std::coroutine_handle<> h) const {
                target.threadPool->enqueue([h] { h.resume(); });
            }
            void await_resume() const noexcept {}
        };
        return ThreadSwitchAwaiter{target};
    }
};

典型用途:

  • 在游戏服务器中,确保协程恢复时回到正确的线程/EventLoop
  • 自动为所有异步操作添加超时检测
  • 禁止协程等待不安全的类型

八、游戏服务器实战场景

8.1 场景一:异步数据库查询

cpp 复制代码
// 将回调式 DB 接口包装为可 co_await 的
template <typename ResultType>
class DBQueryAwaiter {
    std::string sql_;
    DBManager* db_;
    std::optional<ResultType> result_;
    std::exception_ptr exception_;
    std::coroutine_handle<> handle_;

public:
    DBQueryAwaiter(DBManager* db, std::string sql)
        : db_(db), sql_(std::move(sql)) {}

    bool await_ready() const noexcept { return false; }

    void await_suspend(std::coroutine_handle<> h) {
        handle_ = h;
        db_->QueryAsync(sql_, [this](const DBResult& res) {
            if (res.ok) {
                result_ = res.As<ResultType>();
            } else {
                exception_ = std::make_exception_ptr(
                    std::runtime_error("DB query failed: " + res.error));
            }
            // 恢复协程------注意要回到正确的线程
            handle_.resume();
        });
    }

    ResultType await_resume() {
        if (exception_) std::rethrow_exception(exception_);
        return std::move(*result_);
    }
};

// 封装为便利函数
template <typename T>
DBQueryAwaiter<T> QueryDB(DBManager* db, std::string sql) {
    return DBQueryAwaiter<T>{db, std::move(sql)};
}

// 业务代码------读起来就像同步的
Task<void> HandlePlayerLogin(Connection* conn, int accountId) {
    // 每一步都是异步的,但代码是线性的
    auto account = co_await QueryDB<AccountInfo>(db,
        "SELECT * FROM accounts WHERE id=" + std::to_string(accountId));

    if (account.banned) {
        conn->SendError("账号已封禁");
        co_return;
    }

    auto characters = co_await QueryDB<std::vector<CharInfo>>(db,
        "SELECT * FROM characters WHERE account_id=" + std::to_string(accountId));

    conn->SendCharacterList(characters);
}

8.2 场景二:协程化的游戏 AI 行为树

传统 AI 行为树需要手写复杂的状态机,协程可以大幅简化:

cpp 复制代码
// NPC 巡逻行为------看起来像同步脚本
Task<void> PatrolBehavior(NPC* npc) {
    while (npc->IsAlive()) {
        // 巡逻路径上的每个点
        for (const auto& waypoint : npc->GetPatrolPath()) {
            // 移动到目标点------可能需要多帧
            co_await MoveTo(npc, waypoint);

            // 到达后等待一会儿
            co_await SleepFor{std::chrono::seconds{2}};

            // 检查是否发现敌人
            auto* enemy = npc->DetectEnemy(500.0f);  // 500 范围内搜索
            if (enemy) {
                // 切换到战斗行为
                co_await ChaseBehavior(npc, enemy);
                break;  // 战斗结束后重新开始巡逻
            }
        }
    }
}

Task<void> ChaseBehavior(NPC* npc, Entity* target) {
    while (target->IsAlive() && npc->IsAlive()) {
        float distance = npc->DistanceTo(target);

        if (distance > npc->GetAttackRange()) {
            // 追击------移动一帧的距离
            co_await MoveToward(npc, target->GetPosition(), npc->GetMoveSpeed());
        } else {
            // 在攻击范围内------执行攻击
            co_await PerformAttack(npc, target);
            // 攻击冷却
            co_await SleepFor{std::chrono::milliseconds{npc->GetAttackCooldown()}};
        }

        // 超出追击距离,放弃
        if (distance > 2000.0f) {
            npc->Shout("逃跑了吗...");
            co_return;
        }
    }
}

// MoveTo 的实现------每帧推进一步,到达后恢复协程
struct MoveTo {
    NPC* npc;
    Vec3 target;

    bool await_ready() const noexcept {
        return npc->GetPosition().DistanceTo(target) < 1.0f;  // 已到达
    }

    void await_suspend(std::coroutine_handle<> h) {
        // 注册到移动系统,每帧 tick 推进位置,到达后 resume
        npc->GetMoveSystem()->StartMove(npc, target, [h] {
            h.resume();
        });
    }

    void await_resume() noexcept {}
};

8.3 场景三:协程化的任务/剧情系统

cpp 复制代码
// 一个新手引导任务链
Task<void> BeginnerQuest(Player* player) {
    // 第1步:显示对话
    co_await ShowDialog(player, "NPC_村长", "欢迎来到远征世界!请先去消灭5只野猪。");

    // 第2步:等待玩家完成击杀目标
    co_await WaitForKill(player, MOB_WILD_BOAR, 5);

    // 第3步:给予奖励
    player->AddItem(ITEM_IRON_SWORD, 1);
    player->AddExp(100);

    co_await ShowDialog(player, "NPC_村长", "干得好!这把铁剑送给你。");

    // 第4步:引导去下一个 NPC
    co_await ShowDialog(player, "NPC_村长", "去找铁匠学习锻造吧。");
    co_await WaitForTalkTo(player, "NPC_铁匠");

    co_await ShowDialog(player, "NPC_铁匠", "来,我教你打造第一件装备。");

    // 任务完成
    player->CompleteQuest(QUEST_BEGINNER);
}

// WaitForKill 的实现
struct WaitForKill {
    Player* player;
    int mobId;
    int count;

    bool await_ready() const noexcept { return false; }

    void await_suspend(std::coroutine_handle<> h) {
        int killed = 0;
        // 监听玩家击杀事件
        player->OnKill.Subscribe([=, &killed](int killedMobId) mutable {
            if (killedMobId == mobId) {
                ++killed;
                player->SendQuestProgress(killed, count);
                if (killed >= count) {
                    h.resume();  // 击杀够了,恢复协程
                    return false; // 取消订阅
                }
            }
            return true;  // 继续监听
        });
    }

    void await_resume() noexcept {}
};

8.4 场景四:并发等待多个异步操作

cpp 复制代码
// WhenAll ------ 等待多个 Task 全部完成
template <typename... Tasks>
Task<std::tuple<typename Tasks::value_type...>> WhenAll(Tasks... tasks);

// 使用示例:并行加载玩家数据
Task<void> LoadPlayerData(Player* player) {
    // 三个 DB 查询并发执行
    auto [bag, skills, quests] = co_await WhenAll(
        QueryDB<BagData>(db, "SELECT * FROM bags WHERE player_id=" + playerId),
        QueryDB<SkillData>(db, "SELECT * FROM skills WHERE player_id=" + playerId),
        QueryDB<QuestData>(db, "SELECT * FROM quests WHERE player_id=" + playerId)
    );

    player->InitBag(bag);
    player->InitSkills(skills);
    player->InitQuests(quests);
}

九、协程帧分配与 HALO 优化

9.1 协程帧的堆分配

默认情况下,每次创建协程都会在堆上分配协程帧:

cpp 复制代码
// 编译器生成的帧分配
auto* frame = ::operator new(frame_size);
// frame_size = sizeof(promise) + sizeof(所有局部变量) + 对齐填充 + 编译器簿记

可以在 promise_type 中自定义分配:

cpp 复制代码
struct promise_type {
    // 自定义帧分配------例如从内存池分配
    static void* operator new(size_t size) {
        return CoroutinePool::Allocate(size);
    }

    static void operator delete(void* ptr, size_t size) {
        CoroutinePool::Deallocate(ptr, size);
    }

    // 带参数的 operator new------可以拿到协程函数的参数
    // 如果协程参数中有 allocator/resource,可以用它来分配
    static void* operator new(size_t size, Player* player, auto&&...) {
        return player->GetArena()->allocate(size, alignof(std::max_align_t));
    }
};

9.2 HALO 优化(Heap Allocation eLision Optimization)

编译器可以在某些条件下完全消除协程帧的堆分配,将帧嵌入调用者的栈帧中:

复制代码
HALO 优化条件(非标准,因实现而异):
1. 协程的生命周期完全被调用者包含
2. 编译器能在编译期确定协程帧大小
3. 协程没有逃逸到调用者之外(handle 没被保存到全局/堆上)
cpp 复制代码
// 容易被 HALO 优化的模式
Task<int> Inner() { co_return 42; }

Task<int> Outer() {
    int x = co_await Inner();  // Inner 的帧可能被消除
    co_return x;
}

// 不容易被优化的模式
Task<int> EscapingCoroutine() {
    auto task = Inner();
    globalStorage.push_back(std::move(task));  // handle 逃逸
    co_return 0;
}

十、使用注意事项与陷阱

10.1 悬垂引用------协程的头号陷阱

cpp 复制代码
// 致命错误:引用参数在协程挂起后可能已失效
Task<void> ProcessData(const std::vector<int>& data) {
    co_await SomeAsyncWork();
    // 恢复时 data 可能已经被销毁!
    for (auto v : data) { /* 未定义行为 */ }
}

// 调用处
void Caller() {
    std::vector<int> temp = {1, 2, 3};
    auto task = ProcessData(temp);
    // temp 在这里销毁,但协程还持有它的引用!
}

修复方案:协程参数用值传递或确保引用对象的生命周期覆盖协程全程

cpp 复制代码
// 正确:值传递
Task<void> ProcessData(std::vector<int> data) {
    co_await SomeAsyncWork();
    for (auto v : data) { /* 安全:data 在协程帧中 */ }
}

// 正确:用 shared_ptr 延长生命周期
Task<void> ProcessData(std::shared_ptr<std::vector<int>> data) {
    co_await SomeAsyncWork();
    for (auto v : *data) { /* 安全 */ }
}

10.2 线程安全------恢复线程不确定

cpp 复制代码
Task<void> UnsafeExample() {
    // 此时在 Thread-1
    printf("Before: thread %d\n", GetCurrentThreadId());

    co_await SomeAsyncIO();  // IO 完成回调可能在 IO 线程上

    // 此时可能在 Thread-2!
    printf("After: thread %d\n", GetCurrentThreadId());

    // 如果这里访问了 Thread-1 专属的数据结构------数据竞争!
    threadLocalData.modify();  // 危险!
}

修复方案:在 Awaiter 中确保恢复到正确的线程

cpp 复制代码
struct ResumeOnEventLoop {
    EventLoop* targetLoop;

    void await_suspend(std::coroutine_handle<> h) {
        asyncIO->Start([h, loop = targetLoop]() {
            // IO 线程上完成,投递回目标 EventLoop
            loop->Post([h]() { h.resume(); });
        });
    }
};

10.3 协程生命周期管理

cpp 复制代码
// 错误:fire-and-forget 但没人持有 Task → 协程帧泄漏
void StartWork() {
    SomeCoroutine();  // 返回的 Task 被丢弃!
    // 如果 Task 析构时 destroy 了 handle,协程还没执行完就被销毁了
    // 如果 Task 析构时不 destroy,协程帧泄漏
}

正确做法:

cpp 复制代码
// 方案 A:调用者持有 Task 直到完成
Task<void> Caller() {
    auto task = SomeCoroutine();
    co_await task;  // 等待完成
}

// 方案 B:使用专门的 fire-and-forget 类型
struct FireAndForget {
    struct promise_type {
        FireAndForget get_return_object() { return {}; }
        std::suspend_never initial_suspend() noexcept { return {}; }
        std::suspend_never final_suspend() noexcept { return {}; }
        void return_void() {}
        void unhandled_exception() { std::terminate(); }
        // initial 和 final 都不挂起 → 协程自行运行和清理
    };
};

FireAndForget StartBackgroundWork() {
    co_await SomeAsyncWork();
    // 完成后协程帧自动销毁
}

10.4 异常安全

cpp 复制代码
Task<void> LeakyCoroutine() {
    auto* raw = new BigObject();
    co_await SomeAsyncWork();   // 如果这里抛异常...
    delete raw;                  // 这一行永远不会执行 → 内存泄漏
}

// 修复:使用 RAII
Task<void> SafeCoroutine() {
    auto obj = std::make_unique<BigObject>();
    co_await SomeAsyncWork();   // 异常时 obj 自动析构
    obj->DoSomething();
}

十一、协程与传统方案对比总结

维度 回调 状态机 有栈协程(Boost) C++20 无栈协程
代码可读性 差(嵌套地狱) 中(分散但可控) 好(同步风格) 好(同步风格)
内存开销 中(状态成员变量) 高(每协程几百KB栈) 低(帧按需分配)
上下文切换 重(保存/恢复寄存器) 轻(普通函数调用)
可扩展性 高(无阻塞) 中(栈内存受限)
调试体验 中(工具链在改善)
标准库支持 第三方 C++20 标准
编译器优化 有限 一般 无法优化 可 HALO 消除堆分配
学习成本 高(Promise/Awaiter)
最佳场景 简单的一次性异步 明确的状态转换 大量阻塞式遗留代码 复杂异步流程编排

十二、思考题

  1. 生命周期陷阱:以下代码有什么问题?如何修复?
cpp 复制代码
Task<std::string_view> GetName(const Player& player) {
    co_await LoadPlayerData(player.id);
    co_return player.GetName();  // 返回 string_view
}

Task<void> PrintName() {
    auto name = co_await GetName(Player{123});
    printf("Name: %s\n", name.data());  // 安全吗?
}
  1. 调度选型:游戏服务器中有两种常见的协程调度模型:

    • (a) 单线程 EventLoop + 协程(所有协程在同一线程恢复)
    • (b) 线程池 + 协程(协程可能在任意线程恢复)

    各自的优缺点是什么?游戏逻辑线程应该选哪种?

  2. 性能权衡:对比以下两种实现游戏 AI 巡逻逻辑的方式,在 10000 个 NPC 同时巡逻的场景下,内存和 CPU 开销各有什么差异?

    • (a) 每个 NPC 一个协程(PatrolBehavior 协程)
    • (b) 统一的 tick 函数 + 状态枚举

十三、思考题参考答案

题 1:GetName 返回 string_view 的生命周期问题

答:有两个问题------引用参数悬垂和 string_view 悬垂。

问题一:const Player& player 引用悬垂

cpp 复制代码
Task<void> PrintName() {
    auto name = co_await GetName(Player{123});
    //                          ^^^^^^^^^^^
    // Player{123} 是临时对象,在 co_await 表达式完成后就销毁了
    // 但 GetName 的协程帧中持有的是引用------指向已销毁的临时对象
}

Player{123} 是一个临时对象。协程函数的参数是引用类型时,编译器只在协程帧中存储引用(指针) ,不会拷贝对象本身。当协程在 co_await LoadPlayerData(...) 处挂起时,临时对象的生命周期已经结束,之后恢复时 player 引用指向的是已析构的内存。

问题二:string_view 悬垂

即使修复了参数问题(改为值传递 Player),co_return player.GetName() 返回的 string_view 指向 Player 内部的字符串。当 GetName 协程结束后,其帧被销毁(包括帧中的 Player 拷贝),string_view 就悬垂了。

cpp 复制代码
auto name = co_await GetName(Player{123});
// 此时 GetName 的协程帧已经销毁
// name (string_view) 指向的字符串已经不存在了
printf("Name: %s\n", name.data());  // 未定义行为!

修复方案:

cpp 复制代码
// 修复1:参数值传递 + 返回 string 而非 string_view
Task<std::string> GetName(Player player) {  // 值传递,拷贝到协程帧
    co_await LoadPlayerData(player.id);
    co_return std::string(player.GetName());  // 返回拥有所有权的 string
}

// 修复2:如果不想拷贝 Player,使用 shared_ptr
Task<std::string> GetName(std::shared_ptr<Player> player) {
    co_await LoadPlayerData(player->id);
    co_return std::string(player->GetName());
}

核心原则:

  • 协程参数尽量值传递,避免引用或指针指向可能在挂起期间销毁的对象
  • 协程的 co_return 不要返回指向协程帧内局部数据的引用/view,因为帧会被销毁
  • 如果必须用引用,确保引用对象的生命周期严格长于协程的整个执行周期

题 2:协程调度模型选型

答:游戏逻辑线程选 (a) 单线程 EventLoop + 协程。

方案 (a) 单线程 EventLoop + 协程:

复制代码
EventLoop (单线程)
┌─────────────────────────────────────┐
│  事件队列: [协程恢复, IO完成, 定时器] │
│                                     │
│  while (running) {                  │
│      event = queue.pop();           │
│      event.execute();  // 可能 resume 某个协程  │
│  }                                  │
│                                     │
│  所有协程都在同一线程恢复             │
│  → 无需加锁                          │
│  → 可以安全访问所有游戏数据           │
└─────────────────────────────────────┘
优点 缺点
无锁,无数据竞争 单线程无法利用多核
游戏对象可以自由交互 长时间计算会阻塞整个 Loop
调试简单,执行顺序确定 吞吐量受限于单核性能
恢复线程确定,不用担心上下文切换问题 需要将 CPU 密集任务卸载到工作线程

方案 (b) 线程池 + 协程:

复制代码
线程池
┌──────────┐ ┌──────────┐ ┌──────────┐
│ Thread-1 │ │ Thread-2 │ │ Thread-3 │
│ 协程 A   │ │ 协程 B   │ │ 协程 C   │
│ 协程 D   │ │          │ │ 协程 E   │
└──────────┘ └──────────┘ └──────────┘
                ↕ 协程可能在不同线程恢复
优点 缺点
充分利用多核 共享数据必须加锁
吞吐量高 协程恢复线程不确定,容易引发竞态
适合 IO 密集型服务 游戏对象交互需要额外同步机制
调试困难,执行顺序不确定

为什么游戏逻辑线程选 (a)?

游戏逻辑(战斗计算、技能释放、背包操作等)本质上是大量对象间的密集交互------一个技能释放可能涉及施法者、目标、周围所有玩家、buff 系统、伤害系统等多个子系统。如果这些在多线程中执行,加锁的复杂度和性能开销会非常大。

推荐架构:

复制代码
游戏逻辑:单线程 EventLoop + 协程(方案 a)
IO / DB:线程池(方案 b)或独立 IO 线程

GameLoop (Thread-1)          IO 线程池
┌────────────────────┐      ┌──────────┐
│ 协程: HandleLogin  │─────→│ DB 查询  │
│   co_await DB查询   │      │          │
│   (挂起)           │      │ 完成后:  │
│                    │←─────│ Post回   │
│   (恢复,继续逻辑)   │      │ GameLoop │
└────────────────────┘      └──────────┘

DB 查询在 IO 线程池执行,完成后将恢复操作投递回游戏逻辑线程的 EventLoop,确保协程始终在同一线程恢复。


题 3:协程 vs 状态枚举在 10000 NPC 巡逻场景下的对比

答:各有优劣,具体数据如下。

方案 (a):每个 NPC 一个协程

cpp 复制代码
Task<void> PatrolBehavior(NPC* npc) {
    while (npc->IsAlive()) {
        for (const auto& wp : npc->GetPatrolPath()) {
            co_await MoveTo(npc, wp);
            co_await SleepFor{2s};
            if (auto* e = npc->DetectEnemy(500.0f)) {
                co_await ChaseBehavior(npc, e);
                break;
            }
        }
    }
}

内存开销分析:

  • 每个协程帧:约 100~300 字节(局部变量 + promise + 编译器簿记)
  • 10000 个 NPC:约 1~3 MB
  • 额外开销:每个协程帧一次堆分配(可通过自定义 allocator 缓解)

CPU 开销分析:

  • 每帧只有"需要动作"的 NPC 被 resume,等待中的协程开销为零
  • resume 一个协程 ≈ 一次间接函数调用 + 恢复局部变量
  • 如果大部分 NPC 在 SleepForMoveTo 中等待,CPU 开销极低

方案 (b):统一 tick + 状态枚举

cpp 复制代码
struct NPCPatrolState {
    enum State { MOVING, WAITING, CHASING } state;
    int waypointIndex;
    float waitTimer;
    // 更多中间状态...
};

void TickAllNPCs(std::vector<NPC>& npcs, float dt) {
    for (auto& npc : npcs) {
        switch (npc.patrolState.state) {
            case MOVING:
                MoveStep(npc, dt);
                if (Arrived(npc)) npc.patrolState.state = WAITING;
                break;
            case WAITING:
                npc.patrolState.waitTimer -= dt;
                if (npc.patrolState.waitTimer <= 0) {
                    npc.patrolState.waypointIndex++;
                    npc.patrolState.state = MOVING;
                }
                break;
            // ...
        }
    }
}

内存开销分析:

  • 每个 NPC 的 PatrolState:约 16~32 字节(枚举 + 几个字段)
  • 10000 个 NPC:约 160~320 KB
  • 无堆分配开销

CPU 开销分析:

  • 每帧遍历所有 10000 个 NPC,即使大部分什么都不做
  • 数据布局紧凑,缓存友好(连续内存遍历)
  • switch/case 分支预测友好

对比总结:

维度 协程方案 状态枚举方案
内存 (10000 NPC) 1~3 MB 160~320 KB
CPU(大部分空闲时) 几乎零(只 resume 活跃的) 遍历全部,但每次极快
CPU(全部活跃时) 类似 可能更快(缓存友好)
代码复杂度 低(线性逻辑) 高(状态机维护复杂)
行为扩展性 高(加 co_await 即可) 低(状态爆炸)
堆分配 每个协程一次

实际建议:

  • 如果 AI 行为简单(巡逻、追击、返回),状态枚举更轻量
  • 如果 AI 行为复杂(多阶段 Boss 战、带条件分支的行为树),协程大幅降低代码复杂度
  • 折中方案:简单 NPC 用状态枚举,复杂 Boss/剧情 NPC 用协程
  • 可以对协程帧使用自定义内存池来减少堆分配开销

参考资料

相关推荐
星幻元宇VR1 小时前
VR党建互动平台助力党建学习提质增效
科技·学习·安全·vr·虚拟现实
星幻元宇VR2 小时前
VR施工安全行走平台:沉浸式安全教育新方式
科技·学习·安全·生活·vr
jrlong2 小时前
HelloAgents 进阶篇 task02
笔记·学习
GHL2842710902 小时前
playwright学习
学习·ai
艾莉丝努力练剑2 小时前
【Linux线程】Linux系统多线程(七):<线程同步与互斥>线程同步(下)
java·linux·运维·服务器·c++·学习·操作系统
今儿敲了吗2 小时前
ArkTS 声明式 UI 开发
笔记·学习
嵌入式小企鹅2 小时前
国芯抗量子MCU突破、太空算力元年开启、AI编程工具密集发布
学习·ai·边缘计算·算力·risc-v·芯片·半导体
凯尔萨厮3 小时前
Maven学习笔记
笔记·学习·maven
OSwich3 小时前
【 Godot 4 学习笔记】运算符
笔记·学习·godot