C++协程入门

co_routine协程入门

自学协程笔记,旨在用自己能接受的简单的语言一步一步详细的认知协程,了解运转流程。学知有限,内容可能有误,欢迎指出。

更专业的请参考BennyHuo的视频和博客。

什么是协程

普通函数:像一次性说完一整段话,中间不能停,说完了就结束。

协程 :像打电话时"你等一下,我查个资料,别挂",查完继续聊。可以暂停 ,稍后恢复 ,能多次这样操作。

下面给出协程所需的结构类型大概浏览,不需要你现在知道什么意思。

cpp 复制代码
struct Task {
    struct promise_type {
        // 创建协程时调用
        Task get_return_object() {
            return Task{std::coroutine_handle<promise_type>::from_promise(*this)};
        }
        
        // 初始挂起点(true=立即暂停,false=立即执行)
        std::suspend_always initial_suspend() { return {}; }
        
        // 最终挂起点(true=暂停,false=不暂停)
        std::suspend_always final_suspend() noexcept { return {}; }
        
        // 处理 co_return 的值
        void return_void() {}
        
        // 处理异常
        void unhandled_exception() { std::terminate(); }
    };
    
    std::coroutine_handle<promise_type> handle;
    
    Task(std::coroutine_handle<promise_type> h) : handle(h) {}
    ~Task() { if (handle) handle.destroy(); }
    
    // 恢复协程
    void resume() {
        if (handle && !handle.done()) {
            handle.resume();
        }
    }
};

/*
	我们这里折叠简化一下
*/

struct Task {
    struct promise_type {
        Task get_return_object();
        std::suspend_always initial_suspend();
        std::suspend_always final_suspend();
        void return_void() ;
        void unhandled_exception();
    };
    std::coroutine_handle<promise_type> handle;
	...
};

经过折叠之后的结构很清晰,我们可以看到外围的Task结构体和内部的promise_type结构体。外部的结构体可以随意取名,内部的promise_type是C++语法要求。

当然我们也可以分开写,随便定义内部结构体的名字,只需要Task使用别名promise_type代替那个结构体。如下:

cpp 复制代码
struct 随便取一个名字 {
        Task get_return_object();
        std::suspend_always initial_suspend();
        std::suspend_always final_suspend();
        void return_void() ;
        void unhandled_exception();
};

struct Task {
    // 一定要内部有promise_type
    using promise_type = 随便取一个名字;
    std::coroutine_handle<promise_type> handle;
	...
};

上述结构只是协程需要的一个返回类型,但并不是协程。真正的协程函数如下:

cpp 复制代码
Task myCoroutine() {
    std::cout << "协程开始\n";
    co_await std::suspend_always{};  // 暂停
    std::cout << "协程恢复\n";
    co_await std::suspend_always{};  // 再暂停
    std::cout << "协程结束\n";
}

如何判断一个函数是协程呢?就是函数内一定要有co_await/co_yield/co_return其中之一一个关键字。

然后协程呢返回类型一定是我们上面定义的Task类型。为什么一定要有这么一个Task类型呢?可以想象,一个可以自由暂停和恢复的函数必然要有复杂的控制,如果没有一个可以告诉我们何时恢复,如何恢复的信息,必然是不可取的。

上面的只是给大家一个概览,

  • 协程需要一个结构体Task(可以自定义名字)

  • Task内部需要有满足C++要求的promise_type.

  • 协程基本结构,返回类型是Task,

  • 协程执行体内需要有特殊的关键字。

    好,这就是协程的样子,那我们就开始入门吧。

promise_type

作为协程的返回类型Task内部的结构体promise_type,同时也是C++语法要求的存在,必然起着十分重要的作用。他对协程的启动,挂起,结束等起着很重要的作用。

我们来看他的结构,根据C++语法要求,必需要实现下面四个函数。

cpp 复制代码
struct promise_type {
    Task get_return_object();
    std::suspend_always initial_suspend();
    std::suspend_always final_suspend();
    void unhandled_exception();
};
  • get_return_object
cpp 复制代码
Task get_return_object() {
    return Task{std::coroutine_handle<promise_type>::from_promise(*this)};
}

这个函数用于外部的Task获取指向协程帧的handle。他会在协程创建的时候,自动调用。Task的成员函数,协程的句柄handle也是自动根据这个生成的.

借助AI给出一个较为清晰的流程图

bash 复制代码
// 编译器生成的伪代码
Task my_coroutine() {
    // 1. 分配协程帧(包含 promise 对象)
    coroutine_frame* frame = operator new(sizeof(coroutine_frame));
    
    // 2. 构造 promise 对象(在协程帧内)
    new (&frame->promise) Task::promise_type();
    
    // 3. 获取协程帧的句柄(这里才是句柄的"来源")
    // 句柄本质上是指向 frame 的指针包装
    auto handle = std::coroutine_handle<Task::promise_type>::from_address(frame);
    
    // 4. 调用 get_return_object(),传入 handle
    // 注意:from_promise(*this) 只是从 promise 反推回 handle
    Task return_object = frame->promise.get_return_object();
    
    return return_object;
}


协程帧 (堆内存)
    ↓
promise 对象 (在协程帧内)
    ↓
from_promise(*this) 获取 handle
    ↓
get_return_object() 返回 Task
    ↓
Task 构造函数接收 handle
    ↓
task 对象存储 handle

std::coroutine_handle 到底是什么呢?用一个比喻来说,他就是协程的"遥控器"。手里拿着他,你可以的对协程进行恢复(resume()),销毁(destroy()),检查是否结束(done()),访问我们的promise_type(promise())。

get_return_object可以从当前的promise_type来创建出一个handle,指向真正的协程。promise_type是协程内部和协程外部的核心桥梁。

bash 复制代码
                    外部世界
                   (用户代码)
                       ▲
                       │ ① 通过 Handle 访问
                       │
              ┌────────┴────────┐
              │    Promise      │  ◄── 桥梁
              │  (行为规范)      │
              └────────┬────────┘
                       │ ② 控制协程行为
                       │
                       ▼
                  协程内部
                 (协程体代码)
  • initial_suspend
cpp 复制代码
std::suspend_always initial_suspend() { return {}; }

这个函数在协程创建开始的时候调用,根据这个函数的返回值,决定协程开始的时候挂不挂起,也就是一开始到底执不执行。

这时候我们注意到了std::suspend_always 这个结构。除了他之外,还有一个std::suspend_never结构。他们也是C++提供给我们的结构体,同时满足一定的结构。

下面是两个结构体的结构:

cpp 复制代码
struct suspend_always
{
    constexpr bool await_ready() const noexcept { return false; }

    constexpr void await_suspend(coroutine_handle<>) const noexcept {}

    constexpr void await_resume() const noexcept {}
};

struct suspend_never
{
    constexpr bool await_ready() const noexcept { return true; }

    constexpr void await_suspend(coroutine_handle<>) const noexcept {}

    constexpr void await_resume() const noexcept {}
};

我们发现这两个结构体几乎一模一样,唯一不同的是await_ready的返回值。一个是true,一个是false.

满足这两个结构体的结构的类型我们叫做等待体(我们之后也可以自定义等待体),顾名思义,他是用来决定协程是否等待的。

根据我们前面说的,执行到initial_suspend的时候,我们返回了一个等待体。然后协程根据等待体的await_read决定是否挂起。如果await_ready返回true,我们可以简单的想象:**:你ready好了吗?:true!**也就是准备好了(ง •̀_•́)ง,那么就会直接返回继续执行协程,也就是协程不会挂起。

如果await_ready返回false,那就是**:你ready好了吗?:false.** 那么协程没有准备好就会转而执行下面的另一个函数await_suspend,执行完毕之后,协程就开始挂起。然后控制权限返回给调用者。

等协程恢复,await_resume就会被函数调用。

给出下面的示意代码:

cpp 复制代码
initial_suspend()->awaiter(等待体)
if (awaiter.await_ready() == true){
    协程继续,执行协程函数体内的代码
}else if (awaiter.await_ready() == false){
    await_suspend();
    return返回给调用者。
}
  • final_suspend
cpp 复制代码
std::suspend_always final_suspend() noexcept { return {}; }

这个函数是在整个协程结束之后调用的,同样也返回一个等待体,用于告诉协程结束后是否需要挂起。一般我们使用std::suspend_always,表示结束后总是挂起。挂起的好处是可以让外部的Task去进行手动的清理资源,防止悬空引用。否则可以想象,我们外部的Task还持有协程的句柄,但是协程结束后直接清理资源了,那我们Task手里的handle不就是悬空的吗,如果使用直接出错。

  • unhandled_exception
cpp 复制代码
void unhandled_exception() { std::terminate(); }

这个函数用于处理异常。我们可以选择不同的方式进行处理。这里我们使用了直接结束程序。当然我们也可以在我们的promise_type内部创建一个变量,然后unhandled_exception用于将异常保存在这个变量上,也是一种常见的做法。示意:

cpp 复制代码
struct promise_type{
  	std::exception_ptr e;
    void unhandled_exception() noexcept { 
        e = std::current_exception();
    }
    ...
};

初次尝试协程

ok啊,讲完了基本的概念,我们可以先尝试一下协程的效果。我们用AI写了一个简单的例子:

cpp 复制代码
#include <coroutine>
#include <iostream>
#include <thread>

// 1. 定义返回类型(包含 promise_type)
struct Task {
    struct promise_type {
        Task get_return_object() {
            return Task{std::coroutine_handle<promise_type>::from_promise(*this)};
        }
        std::suspend_always initial_suspend() { return {}; }
        std::suspend_always final_suspend() noexcept { return {}; }
        void unhandled_exception() { std::terminate(); }
    };
    std::coroutine_handle<promise_type> handle;
    Task(std::coroutine_handle<promise_type> h) : handle(h) {}
    ~Task() { if (handle) handle.destroy(); }
};
Task myCoroutine() {
    std::cout << "协程开始\n";
    co_await std::suspend_always{};  
    std::cout << "协程恢复\n";
    co_await std::suspend_always{};
    std::cout << "协程结束\n";
}
int main() {
    Task task = myCoroutine();
    
    task.handle.resume();  // 输出:协程开始
                    // 遇到 co_await 暂停
    
    task.handle.resume();  // 输出:协程恢复
                    // 再遇到 co_await 暂停
    
    task.handle.resume();  // 输出:协程结束
    
    return 0;
}

Task就是我们定义的协程返回类型,内部有promise_type.

当主程序main执行Task task = myCoroutine(); 就算是真正的创建了协程。根据我们上面的介绍,这时候就要执行promise_type的initial_suspend函数。检测到返回值std::suspend_always所以直接挂起,协程刚开始不运行。

这时候控制权限交还给我们的调用者,也就是main,这时候继续往下走,假如此时main中没有其他的代码操作,直接就结束了。因为协程挂起,mian往下走直接运行结束。

但是我们此时main执行了task.handle.resume();,何意味呢?

cpp 复制代码
---->task.handle
    返回Task内部的std::coroutine_handle<promise_type> handle;指向真正的协程帧
	这个成员是协程创建的时候,调用协程创建好的promise对象内部的get_return_object传入给Task
---->task.handle.resume()
   	使用拿到的handle遥控器执行resume()恢复协程运行。

此时协程继续运行进入到函数体内部执行输出"协程开始\n"。然后运行到co_await std::suspend_always{};

前面说过co_await/co_return/co_yield都是标志协程的关键字,他的作用是根据后面的等待体的内容,判断协程接下来的行为。

我们知道std::suspend_always意味着挂起(await_ready()==false)。所以执行到co_await std::suspend_always{}这里的时候协程又挂起了。控制权返回给main.如果main没有别的操作,依然如刚开始假设那样直接结束。

而我们这里再次执行了task.handle.resume();,那么协程恢复运行再次继续执行输出"协程恢复\n"。

下面也同理,运行到co_await std::suspend_always{};,协程挂起,回到main,task.handle.resume();恢复协程,执行输出"协程结束\n"。

最后回到main,main后面没有其他的操作了,于是程序结束。需要注意的是,当离开作用域的时候,这个Task执行析构函数

cpp 复制代码
~Task() { if (handle) handle.destroy(); }

他会检测指向协程的句柄handle是否存在,如果存在那就清理掉。这和我们上面final_suspend连上了,final_suspend我们返回std::suspend_never挂起,handle不会被清理。因此交给我们这里Task管理。

最终结果

bash 复制代码
协程开始
协程恢复
协程结束

promise_type::final_suspend返回值导致的生命周期问题

上面提到了final_suspend和Task析构的关系。我们这里就举个反例。

假如我们这个协程是需要返回值,那么我们需要在promise_type内部保存这个值,同时我们的final_suspend不再是前面提到的返回std::suspend_always ,而是std::suspend_never.

cpp 复制代码
struct promise_type {
    int value;
    Task get_return_object() {
        return Task{std::coroutine_handle<promise_type>::from_promise(*this)};
    }
    std::suspend_always initial_suspend() { return {}; }
    std::suspend_always final_suspend() noexcept { return {}; }
    void unhandled_exception() { std::terminate(); }
};

那我外部如何获取这个值呢?我们可以使用task.handle.promise().value拿到这个value.因为这个handle可以通过promise()拿到内部promise_type的信息,然后取value即可。

我们测试如下

cpp 复制代码
Task myCoroutine() {
    std::cout << "协程开始\n";
    co_return 5;
}

int main(){
    Task task = myCoroutine();
    task.handle.resume(); 
    std::cout << task.handle.promise().value << std::endl;
}

我们这个测试需要co_return 返回5,但是这个5怎么跟value联系上,让value存储起来呢?那我们就要实现一个函数,让协程支持co_return.(同理co_yield也需要实现类似函数,这里暂时不考虑)

cpp 复制代码
struct promise_type {
    int value;
    Task get_return_object() {
        return Task{std::coroutine_handle<promise_type>::from_promise(*this)};
    }
    std::suspend_always initial_suspend() { return {}; }
    std::suspend_always final_suspend() noexcept { return {}; }
    void unhandled_exception() { std::terminate(); }
    void return_value(int v){
        value = v;
    }
};

这里需要实现一个 return_value 的函数,名字是C++标准要求不必纠结。当我们执行co_return 5 的时候,调用return_value(5),将value设置为了5.

好的万事具备我们来考虑我们的测试。刚开始根据initial_suspend我们协程会挂起,然后我们的main中直接resume协程,协程开始运行直接输出"协程开始\n",然后直接co_return 5.然后协程结束,由于final_suspend返回std::suspend_never的缘故,协程不会挂起,直接清理资源(相当于handle.destroy())。回到main,之后,我们想要输出协程得到的value,于是我们 std::cout << task.handle.promise().value << std::endl

但是这样的话就坏了!刚才提到协程结束,handle.destory()了,也就是task内部的handle是悬空的!这时候使用handle.promise()就会导致段错误!

因此协程中我们一般上promise_type::final_suspend返回std::suspend_always会更加安全。因为handle的生命周期会和外部的Task同步,当Task析构的时候,才会清理掉handle.

但也不是说fianl_suspend永远都要返回std::suspend_always,具体问题具体分析。

自定义Awaiter

前面提到了,co_await可以后面可以跟Awaiter,是一个普通的c++对象,只要实现三个特定函数,比如C++预提供std::suspend_never;std::suspend_always.这里我们可以自定义实现一个Awaiter.

cpp 复制代码
struct awaiter{
    bool await_ready() const noexcept {
        // 返回 true  → 不暂停,直接继续执行
        // 返回 false → 需要暂停,进入 await_suspend
    }
    
    // ② 暂停时做什么?
    void await_suspend(std::coroutine_handle<> handle) const noexcept {
        // 协程即将暂停,这里可以:
        // - 保存 handle 以便后续恢复
        // - 启动异步操作
        // - 调度到其他线程
        // - 传递给回调函数
    }
    
    // ③ 恢复时做什么?返回值是什么?
    T await_resume() const noexcept {
        // 协程恢复时调用
        // 返回值作为 co_await 表达式的结果
        // 可以是 void,也可以是任意类型
    }
};

我们这里给出一个使用awaiter的示例:

cpp 复制代码
#include <chrono>
#include <coroutine>
#include <cstddef>
#include <exception>
#include <future>
#include <iostream>
#include <thread>

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

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

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

        void unhandled_exception() {
            std::terminate();
        }
    };

    std::coroutine_handle<promise_type> handle;

    Task(std::coroutine_handle<promise_type> h) : handle(h) { }

    ~Task() {
        if (handle) {
            handle.destroy();
        }
    }
};

struct awaiter {
    std::shared_future<std::optional<int>> fut;

    bool await_ready() noexcept {
        return false; // 挂起->执行await_suspend
    }

    void await_suspend(std::coroutine_handle<> handle) noexcept {
        std::cout << "异步任务开始" << std::endl;
        auto promise_ptr = std::make_shared<std::promise<std::optional<int>>>();
        fut = promise_ptr->get_future();

        std::thread([handle, promise_ptr]() {
            std::this_thread::sleep_for(std::chrono::seconds(5));
            try {
                promise_ptr->set_value(999);
                handle.resume();
            } catch (...) {
                promise_ptr->set_value(std::nullopt);
                promise_ptr->set_exception(std::current_exception());
            }
        }).detach();
    }

    int await_resume() noexcept {
        return fut.get().value();
    }

};

Task myCoroutine() {
    std::cout << "协程开始\n";
    int value = co_await awaiter{};
    std::cout << "value:" << value << std::endl;
}

int main() {
    Task task = myCoroutine();
    task.handle.resume();

    std::this_thread::sleep_for(std::chrono::seconds(10));
	std::cout << "main exit" << std::endl;
    return 0;
}

Task我们就还复用之前使用的,一就是协程开始先挂起,等我们在main中手动调用task.handle.resume()恢复协程.我们可以知道,协程一恢复,立马输出"协程开始".运行到co_await的时候,根据awaiter的await_ready返回false,然后执行await_suspend函数,之后协程挂起。

简单介绍下await_suspend内部的逻辑。我们在awaiter结构体存储了一个成员变量std::shared_future
fut
,用于同步等待获取异步任务的结果。然后await_suspend我们首先在堆上创建一个promise,将fut和promise关联上。在堆上创建一个promise,是因为我们整个操作是异步的,也就是注册完异步任务直接不阻塞await_suspend操作完协程立马挂起,栈上的变量都会被清除。我们用智能指针创建堆上的promise,可以防止悬空引用。

然后使用线程启动一个异步任务模拟耗时任务,我们把线程分离掉,不去考虑他的析构。我们传给线程的这个任务是一个lambda函数,捕获了当前的协程handle以及promise_ptr. 在线程内部 sleep 5s后,获得结果(prmise_ptr->set_value(999))后恢复协程(handle.resume()).

协程挂起后,恢复调度到main,这时候我们的main开始sleep 10s防止主程序退出。待到5s之后,线程睡眠结束,设置value之后,handle.resume()恢复协程。这时候await_resume()返回值给协程的myCoroutine的value,然后我们打印出value的值。再5s之后main结束。

bash 复制代码
❯ ./co_routine
协程开始
异步任务开始
value:999
main exit

co_yield和co_return的使用

co_yield

co_yield效果是挂起协程,并返回一个值给调用者。实际上,co_yield就是语言为co_await实现的一个小马甲,更加的方便使用。我们要是想要支持co_yield需要在promise_type定义函数yield_value.

那么协程使用co_yield的时候原理:

cpp 复制代码
co_yield value;
// 等价于:
co_await promise.yield_value(value);

实现:

cpp 复制代码
struct Task {
    struct promise_type {
        int value;
        Task get_return_object() {
            return Task{
                std::coroutine_handle<promise_type>::from_promise(*this)};
        }

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

        std::suspend_always final_suspend() noexcept {
            return {};
        }
        
        std::suspend_always yield_value(int value){
            this->value = value;
            return {};
        }

        void unhandled_exception() {
            std::terminate();
        }
    };

    std::coroutine_handle<promise_type> handle;

    Task(std::coroutine_handle<promise_type> h) : handle(h) { }

    ~Task() {
        if (handle) {
            handle.destroy();
        }
    }
};

我们在promise_type内部定义了一个成员value,然后实现了一个函数yield_value,当协程调用co_yield value .的时候就会把value传入yield_value函数,然后赋值给promise_type内部的value.然后返回std::suspend_always协程挂起。

我们做个小测试。

cpp 复制代码
Task myCoroutine() {
    std::cout << "协程开始\n";
    int i = 5;
    while (i--) {
        co_yield i;
    }
}

int main() {
    Task task = myCoroutine();
    task.handle.resume();	// 启动协程

    for(;;){
        if (task.handle.done()) {
            break;
        }
        std::cout << task.handle.promise().value << std::endl;
        task.handle.resume();
    }

    return 0;
}

我们在协程启动之后,循环中co_yield i.第一次co_yield 4,然后协程挂起,恢复到主线程。主线程也是一个循环检测协程的状态,如果协程还没有结束,就把promise_type内部的value输出,然后再次启动协程。

协程启动后,循环继续,co_yield 3,同理,恢复主线程,输出3,然后恢复协程.....如此往复,直到输出0之后,协程恢复,检查到i--为0,循环结束,协程就退出,那么handle.done()==true .这时候主线程检测到task.handle.done(),就break退出,整个程序退出。

bash 复制代码
❯ /home/vivek/Codes/some_implementations/co_routine/bin/debug/co_routine
协程开始
4
3
2
1
0

co_return

同co_yield一样,也需要在promise_type实现一个函数return_value.区别在于co_return只能使用一次,就结束协程返回最终结果

cpp 复制代码
void return_value(int v){
    value = v;
}

当我们使用co_return的时候

cpp 复制代码
{
    ...
    co_return 5;
}
// -> handle.promise().return_value(5);

然后结束协程。基本和co_yield差不多,不多做赘述。

还有一点,co_return还有可能返回的是void类型,这个比较特殊,所以需要特殊的函数进行定义

cpp 复制代码
void return_void(){
	...
}

对返回体的co_await

我们知道,co_await的目标是返回体。但是比如一个协程调用另一个协程,想要对另一个协程进行co_await.有两个方法,一个是在Task的promise_type内部实现一个await_transform 函数,另一个就是在Task重载co_await运算符两种做法:

cpp 复制代码
// 重载co_await
struct Task {
    struct promise_type {
        int value;

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

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

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

        void return_value(int v) {
            value = v;
        }

        void unhandled_exception() { }
    };

    std::coroutine_handle<promise_type> h;

    Task(std::coroutine_handle<promise_type> h) : h(h) { }

    ~Task() {
        if (h) {
            h.destroy();
        }
    }

    struct awaiter {
        Task &task;

        bool await_ready() {
            return task.h.done();
        }

        void await_suspend(std::coroutine_handle<promise_type> caller) {
            // 在新线程中等待 task 完成
            std::thread([caller, this]() {
                while (!task.h.done()) {
                    task.h.resume();
                    std::this_thread::sleep_for(std::chrono::milliseconds(10));
                }
                caller.resume(); // 恢复调用者
            }).detach();
        }

        int await_resume() {
            return task.h.promise().value;
        }
    };
};

auto operator co_await(Task &&task) {
    return Task::awaiter{task};
}


// 实现await_transform
struct Task {
    struct promise_type {
        int value;
        Task get_return_object() {
            return Task{std::coroutine_handle<promise_type>::from_promise(*this)};
        }
        std::suspend_always initial_suspend() { return {}; }
        std::suspend_always final_suspend() noexcept { return {}; }
        
        void return_value(int v) { value = v; }
        void unhandled_exception() {}
        
        auto await_transform(Task&other){
            struct awaiter {
                Task& task;
                bool await_ready() { return task.h.done(); }
                void await_suspend(std::coroutine_handle<> caller) {
                    // 在新线程中等待 task 完成
                    std::thread([caller, this]() {
                        while (!task.h.done()) {
                            task.h.resume();
                            std::this_thread::sleep_for(std::chrono::milliseconds(10));
                        }
                        caller.resume();  // 恢复调用者
                    }).detach();
                }
                int await_resume() { return task.h.promise().value; }
            };
            return awaiter{other};
        }
    };
    
        std::coroutine_handle<promise_type> h;
        Task(std::coroutine_handle<promise_type> h) : h(h) {}
        ~Task() { if (h) h.destroy(); }
    };
}

测试

cpp 复制代码
// 协程A:模拟一个耗时计算
Task slow_calculation() {
    for (int i = 1; i <= 5; ++i) {
        std::this_thread::sleep_for(std::chrono::milliseconds(500));
    }
    co_return 42;
}

// 协程B:模拟另一个耗时计算
Task another_calculation(int input) {
    std::this_thread::sleep_for(std::chrono::seconds(1));
    int result = input * 2;
    co_return result;
}

// 主协程:等待其他协程
Task main_coroutine() {
    int result1 = co_await slow_calculation();
    int result2 = co_await another_calculation(result1);
    int final_result = result1 + result2;
    co_return final_result;
}
using namespace std::chrono_literals;
int main(){
    Task task = main_coroutine();
    task.h.resume();
    std::this_thread::sleep_for(5s);
    
    if (task.done()) {
        int result = task.h.promise().value;
        std::cout << "协程执行完成,最终结果: " << result << std::endl;
    } else {
        std::cout << "协程未完成(可能需要更长的等待时间)" << std::endl;
    }
}

逻辑简单来说就是main_coroutine主协程S启动,首先是co_await第一个协程A,第一个协程由于重载了co_await,他会根据返回的await判断是否挂起。在我们的例子中是挂起走到了await_suspend中,我们在这里面新开了一个线程,线程一直检测,如果task也就是这个协程A一直没有done,那我们就循环检测,然后让协程A一直resume().

也许你有疑问,为什么要循环resume()?在我们这个例子中,协程A与第二个协程B只会挂起一次,只要resume()一次之后,协程就一直执行完了,没有其他的co_await等挂起点。但是实际工作中可能有好多的挂起点,多处都需要唤醒。所以我们这里采用循环唤醒。如果检测到task.h.done(),也就是协程结束了,那么就caller.resume(),这里的caller是await_suspend传进来的handle,实际上就是调用者协程S的handle.

主协程S醒来之后,继续往下执行开始int result2 = co_await another_calculation(result1);,协程B就开始挂起,然后一系列流程就和协程A一样了。待协程B也结束之后,我们就可以计算final_result,然后co_return返回。

我们的main中,显示sleep了5s,防止主程序退出,5s之后检查主协程S是否完毕,是就输出结果。

bash 复制代码
❯ ./co_routine
协程执行完成,最终结果: 126

至此,我们协程的常用操作,结构都已经基本掌握!

简单的序列生成器

简单序列器

简单说明一下,就是可以自动生成指定的数字序列比如生成0-1-2-...,又或者是生成一个斐波那契数列等等。

下面给出基本的结构,先不附加太多东西,我们之后一点一点的添加。

cpp 复制代码
template <typename T>
struct Generator {
    struct promise_type {
        T value;
        bool has_value = false;
        std::exception_ptr e;

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

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

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

        std::suspend_always yield_value(T value) noexcept {
            this->value = value;
            has_value = true;
            return {};
        }

        std::suspend_always return_value(T value) noexcept {
            this->value = value;
            return {};
        }

        T get_value() const noexcept {
            return value;
        }

        std::suspend_always await_transform(T value) {
			this->value = value;
			return {};
        };

        void unhandled_exception() noexcept { 
            e = std::current_exception();
        }

        void set_value(T v) noexcept {
            value = v;
        }
    };

    int get_value() const noexcept {
        return handle.promise().value;
    }

    Generator<int> operator co_await() {
        return handle;
    }

    Generator(Generator const &) = delete;
    Generator &operator=(Generator const &) = delete;
    Generator(Generator &&other) noexcept
        : handle(std::exchange(other.handle, {})) { };

    Generator &operator=(Generator &&other) noexcept {
        handle = std::exchange(other.handle, {});
        return *this;
    }

    std::coroutine_handle<promise_type> handle;

    std::coroutine_handle<promise_type> get_handle() noexcept {
        return handle;
    }

    Generator(std::coroutine_handle<promise_type> h) : handle(h) { }

    bool has_handle = true;

    ~Generator() {
        if (handle && handle.done()) {
            std::cout << "destory" << std::endl;
            handle.destroy();
        }
    }
        
    bool has_next() noexcept {
        if (handle.done()) {
            return false;
        }
        if (!handle.promise().has_value) {
            handle.resume();
        }
        return !handle.done();
    }

    T next() const noexcept {
        handle.promise().has_value = false;
        return handle.promise().value;
    }
};

刚看上去,代码很多很乱,实际上只要抓住我们协程最主要的那几个结构即可。首先是外层的Task,然后定义内部的promise_type结构体。

与前面不同的是我们还在Task定义了许多便利的成员函数,比如get_value(),set_value(),get_handle()等。之前我们获取值的时候都是直接task.handle.promise().value,现在我们定义函数之后,直接task.get_value()即可(虽然之后我们可能没怎么用到。。。)。

还有一个就是结构体使用了模板,这样就不局限于单一的int或者float,而是更加通用了。

了解了这些不同我们就来到了最重要has_next和next函数了。

Task内部有个成员函数 has_value 标志是否有值。我们外部使用的时候调用has_next 成员函数用来检测是否有值,有值的话,就可以调用next获取值。

cpp 复制代码
Generator sequence() {
  int i = 0;
  while (i < 5) {
    co_await i++;
  }
}

int main() {
  auto generator = returns_generator();
  for (int i = 0; i < 15; ++i) {
      if (generator.has_next()){
          std::cout << generator.next() << std::endl;
          continue;
      }
      break;
  }
  return 0;
}
bash 复制代码
1
2
3
4
destory

简单说一下流程,第一步main创建协程之后(auto generator = returns_generator() ),默认has_value = false,协程挂起回到main函数。main循环中,首先判断是否有值(has_next() ),检测到handle没有done,同时!handle.promise().has_value,那么协程就恢复了。协程恢复之后co_await i++ 调用函数await_transform 设置task内部的value为i,然后继续挂起(await_transform返回std::suspend_always)。

这时候has_next()继续进行返回true,main接着就打印出generator.next().如此循环往复,就导致了上述结果。

bash 复制代码
初始状态 → has_value=false, done=false
    ↓
has_next() → resume() → 执行到 co_await
    ↓
挂起状态 → has_value=true, done=false
    ↓
next() → 读取值, has_value=false
    ↓
重复以上过程
    ↓
最终状态 → done=true

我们这个使用的co_await,还需要重载co_await或者实现await_transform函数,每次产生一个值,其实使用co_yield更加方便,只需要实现yield_value函数,所以更加推荐使用yield_value.

斐波那契数列

有了上面的基础,使用协程实现一个斐波那契数列十分的容易:

cpp 复制代码
Generator<int> fibonacci() {
    co_yield 0;
    co_yield 1;

    int a = 0;
    int b = 1;
    while (true) {
        co_yield a + b;
        b = a + b;
        a = b - a;
    }
}

int main() {
    auto g = fibonacci();
    int k = 10;
    while (k-- && g.has_next()) {
        std::cout << g.next() << std::endl;
    }
}

结果如下:

bash 复制代码
./co_routine
0
1
1
2
3
5
8
13
21
34

函数式编程

我们能不能从一个给定的数组或者给定的序列构建这个序列器,然后一个一个来取用呢?当然可以

cpp 复制代码
Generator static from_array(T array[], int n) {
    for (int i = 0; i < n; ++i) {
      co_yield array[i];
    }
}

然后我们就可以这样使用

cpp 复制代码
int array[] = {1, 2, 3, 4};
auto generator = Generator<int>::from_array(array, 4);
while(....){
    std::cout << ...
}

但是不够优雅,还需要手动传入大小,如果传入vector或者list等,就可以直接使用范围for直接遍历,无需手动传入长度。

cpp 复制代码
Generator static from_array(std::vector<T> array) {
    for (auto&p:array) {
		co_yield p;
    }
}

但是如果创建还是需要类似如下操作

cpp 复制代码
Generator::from_array<int>({1,2,3,4});

std::vector<int>vec{1,2,3,4};
Generator::from_array<int>(vec);

如何去掉这个繁琐的{}或者容器呢?我们可以使用变参模板.

cpp 复制代码
template<typename T>
struct Generator {
  ...

  template<typename ...TArgs>
  Generator static from(TArgs ...args) {
    (co_yield args, ...);
  }
}

这里的 (co_yield args, ...) 是折叠表达式,简而言之就是co_yield args是一种模式,表示什么操作。***++...++***则意味着每个参数都要按照前面的模式进行。等价一下代码:

cpp 复制代码
template<typename ...TArgs>
   Generator static from(TArgs ...args) {
       co_yield arg1,co_yield arg2,co_yield 3,.....;
}

简单使用一下吧:

cpp 复制代码
int main() {
    auto p = Generator<int>::from_array(1,2,3,4);
    while(p.has_next()){
        std::cout << p.next() << std::endl;
    }
}

这里创建协程就不需要单独定义一个协程函数了,而是直接根据静态成员函数直接创建,输出结果:

bash 复制代码
1
2
3
4
destory

更多的函数式操作

我们可以添加更多的操作,比如map函数。map操作在其他的语言很常见,就是对传入的所有参数执行同一个变换操作。

cpp 复制代码
template <typename F>
Generator<std::invoke_result_t<F, T>> map(F &&f) {
    while (has_next()) {
        co_yield f(next());
    }
}

我们给这个协程函数传入一个函数f,然后对每一个值进行f操作返回。***std::invoke_result_t<F, T>***可以根据传入的函数类型和操作的参数类型推导出返回的结果类型。

使用案例(对每个值进行*2操作,将每一个值转成std::string )

cpp 复制代码
int main() {
    auto p = Generator<int>::from_array(1,2,3,4).map([](auto i){
        return i * 2;
    }).map([](auto i){
        return std::to_string(i);
    });
    while(p.has_next()) {
        std::cout << p.next() << std::endl;
    }
}

我们协程p的操作根据给出的指定序列对每一个值进行*2然后再进行一次转成std::string的操作,最后循环输出。结果如下:

bash 复制代码
./co_routine
2
4
6
8
destory

有了map操作,其他的操作也就很容易理解和实现了,这里直接给出,你应该能够理解他的意思了!

cpp 复制代码
// 扁平化
/* 	输入: [A, B, C]
     经过 flat_map: 
      A → [a1, a2]
      B → [b1]
      C → [c1, c2, c3]
	结果: [a1, a2, b1, c1, c2, c3]  // 一维数组
*/
template <typename F>
std::invoke_result_t<F, T> flat_map(F &&f) {
    while (has_next()) {
        auto generator = f(next());
        while (generator.has_next()) {
            co_yield generator.next();
        }
    }
}

// 这返回的不是一个协程!操作是每一个数据都进行f操作
template <typename F>
void for_each(F &&f) {
    while (has_next()) {
        f(next());
    }
}

// 多个值结合为一个
template <typename R, typename F>
R fold(R initial, F &&f) {
    R cc = initial;
    while (has_next()) {
        cc = f(cc, next());
    }
    return cc;
}

// 求和
T sum() {
    T sum = 0;
    while (has_next()) {
        sum += next();
    }
    return sum;
}

// 筛选
template <typename F>
Generator filter(F &&f) {
    while (has_next()) {
        T value = next();
        if (f(value)) {
            co_yield value;
        }
    }
}

// 取前n个
Generator take(int n) {
    int i = 0;
    while (has_next() && i < n) {
        co_yield next();
        i++;
    }
}

template <typename Func>
Generator take_while(Func &&f) {
    while (has_next() && f(next())) {
        co_yield next();
    }
}

测试1

cpp 复制代码
int main() {
    auto p = Generator<int>::from_array(1, 2, 3, 4)
                 .map([](auto i) { return i * 2; })		// 2,4,6,8
                 .filter([](auto i) { return i % 2 == 0; })	// 2,4,6,8
                 .take(3)	// 2,4,6
                 .take_while([](auto i) { return i < 10; }); // 2,4,6
    while (p.has_next()) {
        std::cout << p.next() << std::endl;
    }
}
bash 复制代码
./co_routine
2
4
6
destory

测试2

cpp 复制代码
int main() {
    auto p = Generator<int>::from_array(1, 2, 3, 4)
        .flat_map([](auto i) -> Generator<int> {
            for (int j = 0; j < i; ++j) {
                co_yield j;	// 这里每个i值,生成多个又会生成多个协程
            }
        })
        .take(6);

    while (p.has_next()) {
        std::cout << p.next() << std::endl;
    }
}
bash 复制代码
./co_routine
0
destory
0
1
destory
0
1
2
destory
destory

这里最后得到一个协程p,为什么会有这么多的destory呢?其实根据函数的返回值我们都知道,中间的什么take,flat_map其实都会生成协程,不过这个协程是临时的。最后的take生成的协程赋值给了p.

前面的协程因为是临时的协程对象,按道理说返回临时协程之后就应该析构了,输出destroy.但是实际上并没有输出。为什么?因为final_suspend都是返回std::suspend_always结束后协程挂起,然后临时的协程准备析构,检查

cpp 复制代码
~Generator() {
    if (handle && handle.done()) {
        std::cout << "destory" << std::endl;
        handle.destroy();
    }
}

发现handle还没有done。为什么没有done?比如,刚开始from_array(1,2,3,4),一开始只是返回了这个协程,都没有co_yield去消耗,怎么可能done呢?于是不析构。

那么为什么结果中这么多destroy呢?这是因为flat_map而原因,他会根据每个i生成i个临时的协程,每个协程都要co_yield值,当最后消耗完毕之后handle.done就为true,这些临时协程就结束了,于是输出destroy.

最后p(从take得到的协程)因为也完美析构了,也输出了destroy.但是协程调用链条take之前的临时对象都没有析构了(不包括flat_map内部又每次生成的i个协程)。

生命周期问题

cpp 复制代码
template <typename... Args>
static Task from_array(Args &&...args) {
    (co_yield args, ...);
}

第一种实现方式(直接在临时协程上进行操作)

cpp 复制代码
template <typename F>
Task<std::invoke_result_t<F, T>> map(F &&f) {
    while (has_next()) {
        co_yield f(next());
    }
}
auto p = Task<int>::from_array(1, 2, 3, 4).map([](auto i) {
    return std::to_string(i);
});
while (p.has_next()) {
    std::cout << p.next() << std::endl;
}

输出

cpp 复制代码
1
2
3
4
destory
bash 复制代码
1. from_array 创建 A(协程帧在堆上)
2. map 创建 B
3. 临时对象 A 析构(在表达式结束时):
   - A.handle.done() == false(还没执行过)
   - 条件不满足,不销毁 A 的帧! ← 内存泄漏
4. 外部 while 循环驱动 B 执行:
   - B 内部调用 this->has_next(),this 指向已销毁的 A
   - 但 A 的协程帧还在堆上(未销毁)
   - 正常工作
5. B 完成,B 的析构函数执行:
   - B.handle.done() == true → 输出 "destory" → 销毁 B 的帧
6. A 的协程帧呢?没人销毁!永远泄漏了

第二种实现方式(转移所有权)

cpp 复制代码
template <typename F>
Task<std::invoke_result_t<F, T>> map(F &&f) {
    auto up_stream = std::move(*this); // auto推导为Task<int>
    while (up_stream.has_next()) {
        co_yield f(up_stream.next());
    }
}

输出

cpp 复制代码
1
2
3
4
destory
destory
bash 复制代码
1. from_array 创建 A(协程帧在堆上)
2. map 创建 B
3. map 函数体执行(在 B 的上下文中):
   - auto upstream = std::move(*this);  // upstream 持有 A
   - 消费完 A,A 变成 done()
   - 循环结束
4. B 返回,upstream 是 B 的局部变量
5. 外部 while 循环驱动 B 输出值
6. B 完成,B 的析构函数执行:
   - B.handle.done() == true → 输出 "destory" → 销毁 B 的帧
7. B 析构时,upstream 也被析构(B 的局部变量)
   - upstream 是 Task 对象,持有 A 的 handle
   - A.handle.done() == true → 输出 "destory" → 销毁 A 的帧

结论

按照第一种方式,实际上启动了两个不同的协程A/B.不过协程B也就是map启动的协程因为依赖协程A产生值,而A还没有结束,但是这个协程是from_array产生的临时对象,需要被析构,临时对象析构的时候检查A::handle.done()==false,所以不会执行A::handle.destory().而B协程结束后,检查handle.done() == true,所以执行B::handle.destory().

因此,第一种方式只会有一个destory,实际上A的handle泄漏了,因为析构的时候没有destory掉,后续就没有处理了。

而第二种方式,则是协程B直接将协程A的handle转移到了B中,即这里auto up_stream = std::move(*this); 实际这里的up_stream就是Task 类型,就是A被转交给了B.因为B一直存在求值,内部的up_stream也是一直存活的, 也就是实际上协程A还没有走到析构那一步 。当协程B结束的时候,B调用析构函数,执行B::handle.destory(),输出一个 destory,这时候掌管协程A的up_stream也要析构,执行A::handle.destory(),也会输出一个 destory.

实现了这些函数之后,我们可以很方便的进行链式调用(先不考虑协程帧泄漏的问题):

cpp 复制代码
#include "generator.hpp"
#include <iostream>
#include <string>
#include <thread>
#include <vector>

Generator<int> sequnence() {
    int i = 0;
    while (i < 4) {
        co_yield i++;
    }
}

Generator<int> fibonacci() {
    co_yield 0;
    co_yield 1;
    int a = 0, b = 1;
    while (true) {
        co_yield a + b;
        b = a + b;
        a = b - a;
    }
}

struct out {
    template <typename T>
    void operator()(T const &value, char separator = ' ') {
        std::cout << value << separator;
    };
};

struct outln {
    template <typename T>
    void operator()(T const &value) {
        std::cout << value << std::endl;
    };
};

static inline outln outln;
static inline out out;

int main(int, char **) {
    fibonacci()
        .take_while([](auto i) { return i < 20; })
        .filter([](auto i) { return i % 2 == 0; })
        .map([](auto i) { return i * 2; })
        .flat_map([](auto i) -> Generator<int> {
            for (int j = 0; j < i; ++j) {
                co_yield j;
            }
        })
        .for_each([](auto i) { std::cout << i << std::endl; });
}

看上去十分的炫酷。。。

具体调用流程

bash 复制代码
每个操作都创建了一个新的协程:

A: fibonacci (无限流)

B: take_while (包装 A)

C: filter (包装 B)

D: map (包装 C)

E: flat_map (包装 D)

F: for_each (普通函数,不是协程)



1. for_each 驱动 E (flat_map)
   ↓
2. E 从 D 取值
   ↓
3. D 从 C 取值
   ↓
4. C 从 B 取值
   ↓
5. B 从 A 取值
   ↓
6. A 产生一个值 (0, 1, 1, 2, 3, 5, 8, 13...)
   ↓
7. 值向上传递,经过转换
   ↓
8. E 产生一个内层序列 (0,1,2,...,i-1)
   ↓
9. for_each 消费这些值

测试结果如下:

bash 复制代码
destory
0
1
2
3
destory
0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
destory
destory
destory
destory
destory

根据我们上面生命周期那里,按道理而言应该最后才会输出一个destory,为什么这里输出了这么多的destory,有的穿插在中间?

我们换一个小的数值演示

cpp 复制代码
// 0 0 1 2 3
fibonacci()
    .take_while([](auto i) { return i < 3; })
    .filter([](auto i) { return i % 2 == 0; })
    .map([](auto i) { return i * 2; })
    .flat_map([](auto i) -> Generator<int> {
        for (int j = 0; j < i; ++j) {
            co_yield j;
        }
    })
    .for_each([](auto i) { std::cout << i << std::endl; });

我们i<3足够小的序列,斐波那契生成0 0 1 2 可以通过take_while.

然后第一个0,通过filter ,然后通过map ,来到flat_map,这时候这个i还是0.

根据上面的flat_map的定义

cpp 复制代码
template <typename F>
std::invoke_result_t<F, T> flat_map(F &&f) {
    while (has_next()) {
        auto generator = f(next());
        while (generator.has_next()) {
            co_yield generator.next();
        }
    }
}

实际上生成的还是一个协程,也就是执行到flat_map的时候,根据函数f的操作,再次生成好多的协程。

但是我们传入的for循环用来生成指定的序列.

cpp 复制代码
for (int j = 0; j < i; ++j) {
    co_yield j;
}

由于i=0,没有生成协程,那么第一个内层生成器就结束了,销毁了,输出"destory"。接下来回到take_whilefilter ,i0/i1都不满足直接pass.当i2的时候,一路调用下来,到了falt_map此时的i4.根据条件生成了内部值为0 1 2 3 的4个序列器。然后四个分别运行到for_each打印"0 1 2 3",然后结束销毁。再次输出"destory".这时候整个序列器生成完毕,调用链上的所有临时生成的协程一次销毁,共四个(没有for_each),所以再次输出4个"destory"。结果如下:

cpp 复制代码
destory
0
1
2
3
destory
destory
destory
destory
destory

简单的销毁如下:

cpp 复制代码
destory      ← 第一个内层生成器(i=0,空)
0
1
2
3
destory      ← 第二个内层生成器(i=4,完成)
destory      ← take_while 协程 A 完成
destory      ← filter 协程 B 完成
destory      ← map 协程 C 完成
destory      ← flat_map 协程 D 完成