Loading

协程

协程

协程是能暂停执行以在之后恢复的函数。协程是无栈的:它们通过返回到调用方暂停执行,并且恢复执行所需的数据与栈分离存储。

协程调用示意

C++20以后,可以通过co_awaitco_yield去挂起一个协程,通过co_return真正结束一个协程。

  • co_await用于暂停执行,直到恢复;
  • co_yield用于暂停执行并返回一个值;
  • co_return用于完成执行并返回一个值。

一个函数含有上述这些关键字时,那么这个函数就是一个协程。

演示

#include <iostream>
#include <coroutine>

using namespace std;

struct CoRoutineStructure {
    struct promise_type {
        suspend_never initial_suspend() {
            cout << "coroutine initial suspend\n";
            return {};
        }

        suspend_never final_suspend() noexcept {
            cout << "coroutine final suspend\n";
            return {};
        }

        CoRoutineStructure get_return_object() {
            cout << "coroutine get_return_object\n";
            return CoRoutineStructure(coroutine_handle<promise_type>::from_promise(*this));
        }

        void unhandled_exception() { }
    };

public:
    CoRoutineStructure(const coroutine_handle<promise_type>& handle) : m_handle(handle) {
        cout << "construct CoRoutineStruct\n";
    }

    coroutine_handle<promise_type> m_handle;
};

struct Input {
    constexpr bool await_ready() {
        cout << "Input await ready\n";
        return false;
    }

    constexpr void await_suspend(coroutine_handle<CoRoutineStructure::promise_type> h) {
        cout << "Input await suspend\n";
    }

    constexpr int await_resume() {
        cout << "Input await resume\n";
        return 42;
    }
};

CoRoutineStructure guessNumber() {
    Input input;
    int g = co_await input;
    cout << "coroutine: guess " << g << endl;
}

int main() {
    auto ret = guessNumber();
    cout << "main: make a guess...\n";
    ret.m_handle.resume();

    return 0;
}

输出结果:

coroutine get_return_object
construct CoRoutineStruct
coroutine initial suspend
Input await ready
Input await suspend
main: make a guess...
Input await resume
coroutine: guess 42
coroutine final suspend

如果这段代码没有协程的参与,首先调用了getNumber函数,那么就应该先打印
coroutine: guess 42
函数执行完成后返回main函数,再打印后面一句
main: make a guess...

但加入了协程后,一切变得有些不一样了。我们在必要的函数中加入了打印来追踪代码执行的顺序,可以看到在有了协程参与后,程序自己“偷偷”做了很多的事情,最终使得先打印了main函数中的字符串,后打印了guessNumber函数中的字符串。

协程是C++20后才加入标准库中,需要显式加上-std=c++20的选项才能正确编译(至少gcc13.2.0版本是这样)。

概念

执行

当协程启动时,会依次执行下述操作:

  1. operator new 分配协程状态对象;
  2. 将所有的函数形参复制到协程状态中(拷贝、移动、引用);
  3. 调用承诺对象的构造函数,如果没有接收所有形参的构造函数,会调用默认构造;
  4. 调用承诺对象的get_return_object,并将结果保存在局部变量中,会在协程首次暂停时返回给调用方;
  5. 调用initial_suspend方法,并co_wait它的结果;
  6. initial_suspend恢复时,开始协程体的执行。

promise_type

首先需要强行灌输一个C++协程的概念,C++的协程是通过一个内部定义了promise_type结构体的协程体来实现的,如果要使用协程,就需要在自定义的协程体中定义一个名为promise_type的结构体(后续称呼其为承诺对象),并依次提供initial_suspendfinal_suspendget_return_objectunhandled_exception等方法,这四个是必须的。

方法 说明
initial_suspend 决定线程启动时是否立即挂起,标准库中提供了suspend_neversuspend_always分别表示从不挂起和总是挂起,这俩都是支持协程的类,后续我们的实现也要和它们类似,先按下不表。
final_suspend 决定线程结束时是否要挂起,返回值也是一个支持协程操作的对象,同上。
get_return_object 线程挂起时返回的协程体,也就是一个内部含有承诺对象的协程体,这里简单起见就用刚刚定义的这个协程体了,另外,协程体必须要通过一种方式来获取到协程执行过程的信息,标准库中提供了coroutine_handle句柄来封装承诺对象,所以协程体内都要定义一个这样的句柄成员。
unhandled_exception 这个方法会在出现异常时被调用,demo中就定义为空函数了。

自定义的协程类

在guessNumber函数中,我们用co_await来启用协程等待Input这个协程对象。一个类要支持协程,必须提供await_readyawait_suspendawait_resume三个成员函数,正如Input一样。

方法 说明
await_ready 这个方法会被co_await关键字调用。考查suspend_neversuspend_always的效果和实现可以推测,await_ready表示当前协程对象是否准备好了响应await,返回值是布尔类型,如果为true,表示当前的对象已经做好准备了响应wait了,那么就不需要再挂起当前的协程,而是继续执行await_resume方法;如果是false,表示当前的对象还没做好准备,那么就会调用await_suspend方法挂起该对象并切换到别的协程。
await_suspend 在协程挂起时被调用,用于处理当前协程对象挂起时的一些处理操作。关键点在于入参是一个承诺对象的协程句柄,这个协程句柄保存了当前协程的信息,根据这个句柄获取协程的暂停状态和调度协程的恢复或销毁。
await_suspend返回值如果是void或true,表示将控制权返回给当前协程的调用方/恢复方;
如果是false,表示恢复当前协程;
如果返回的是某个某个协程的句柄,通过调用其resume方法恢复该协程。
await_resume 当协程对象在等待后重新获得了协程的控制权,会执行该方法恢复挂起前的状态并继续执行,并且其返回值就是co_await expr中表达式的结果。

co_await

了解了必要的概念后,我们再完整地梳理一下co_await在协程中的工作逻辑:

  1. 首先,协程在一开始就创建了一个承诺对象,也就是我们自定义的promise_type类对象,调用了它的initial_suspend方法来判断是否在协程一开始就挂起,我们返回的是suspend_never表示从不挂起;
  2. 然后继续执行,调用get_return_object方法,返回了含有该承诺对象的句柄的协程体,它将会作为协程函数的返回值传递出去,也就是main函数中接收协程返回的ret;
  3. 下面是协程正式执行,运行到了co_await处,等待的是一个我们自定义的协程对象Input,co_await的执行逻辑就是先判断await_ready方法的返回值决定是否挂起,我们返回的是false,表示挂起;
  4. 挂起后执行await_suspend方法,将当前协程信息保存在了入参的承诺对象句柄中,co_await处于等待状态,协程挂起并回到协程的调用者,也就是main函数;
  5. main函数获取到了传递出来的协程体对象,开始执行调用协程的下一行,打印一行文字,然后使用协程体对象中保存了协程的承诺对象的句柄成员,调用其resume方法唤醒协程;
  6. 协程结束等待,调用await_resume恢复协程的执行,这里我们是将一个整形作为结果传递出去,到这才完成了co_await的一个完整流程,成功给等待co_await赋值的变量g赋上了值,即g等于await_resume的返回值42;
  7. 最后协程打印了结果,完成整个协程的执行并退出。

co_yield

co_yield类似于一个语法糖,等价于

co_await promise.yield_value(表达式)

要使用co_yield,在承诺对象中就要增加一个方法(也可以是不挂起):

suspend_always yield_value(表达式);

例如稍微修改下上面的代码:

int number = 0;

struct CoRoutineStructure {
    struct promise_type {
        // ...

        suspend_always yield_value(int val) {
            number = val;
            return {};
        }
    };

    // ...
};

CoRoutineStructure guessNumber() {
    cout << "coroutine function\n";
    Input input;
    int g = co_await input;
    cout << "coroutine: guess " << g << endl;
    co_yield 13;

}

int main() {
    auto ret = guessNumber();
    cout << "main: make a guess...\n";
    ret.m_handle.resume();
    cout << "main: guess again...\n";
    cout << "number: " << number << endl;
    ret.m_handle.resume();
    return 0;
}

能得到下面的输出:

coroutine get_return_object
construct CoRoutineStruct
coroutine initial suspend
coroutine function
Input await ready
Input await suspend
main: make a guess...
Input await resume
coroutine: guess 42
main: guess again...
number: 13
coroutine final suspend

co_return

co_return表示协程的结束,类似于co_yield,可以返回void或者是一个值,分别要在承诺对象内声明return_voidreturn_value(表达式)

完成猜数字游戏

在main函数中开启猜数字游戏,协程扮演玩家,每猜一个数字就交给main函数判断一下是否猜对了,直至猜中为止。

#include <iostream>
#include <random>
#include <coroutine>
#include <array>

using namespace std;

enum class Status : char {
    start,
    correct,
    less,
    greater,
};

class Judge {
public:
    Judge(int answer, int min, int max) : m_answer(answer), min(min), max(max) { }

    Status operator()() {
        if(m_guess == m_answer) {
            stat = Status::correct;
        }
        else if(m_guess < m_answer) {
            stat = Status::less;
        }
        else if(m_guess > m_answer) {
            stat = Status::greater;
        }
        return stat;
    }

    Status stat = Status::start;
    int min;
    int max;
    int m_guess = -1;

private:
    int m_answer;
};

class CoRoutine {
public:
    struct promise_type {
        promise_type(Judge& j) : judge(j) { }

        suspend_never initial_suspend() {
            return {};
        }

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

        suspend_never final_suspend() noexcept {
            return {};
        }

        suspend_always yield_value(int val) {
            judge.m_guess = val;
            return {};
        }

        void return_void() { }

        void unhandled_exception() { }

    private:
        Judge& judge;
    };

public:
    CoRoutine(const coroutine_handle<promise_type>& handle) : m_handle(handle) { }

    coroutine_handle<promise_type> m_handle;
};

CoRoutine guess(Judge& judge) {
    std::array<int, 2> range{ judge.min, judge.max };
    int number;
    while(judge.stat != Status::correct) {
       if(judge.stat == Status::less) {
            range[0] = number + 1;
        }
        else if(judge.stat == Status::greater) {
            range[1] = number - 1;
        }
        number = (range[0] + range[1]) / 2;
        cout << "guess number: " << number << endl;
        co_yield number;
    }
    co_return;
}

int main() {
    random_device seed;
    default_random_engine eng(seed());
    int answer = uniform_int_distribution<int>(1, 100)(eng);
    cout << "guess a number between 1 and 100.\n";
    Judge judge(answer, 1, 100);
    auto coroutine = guess(judge);
    while(true) {
        auto result = judge();
        if(result == Status::correct) {
            cout << "you're right!" << endl;
            coroutine.m_handle.destroy();
            break;
        }
        else if(result == Status::less) {
            cout << "miss, less than answer.\n";
        }
        else if(result == Status::greater) {
            cout << "miss, greater than answer.\n";
        }
        coroutine.m_handle.resume();
    }

    return 0;
}

我们额外定义了一个Judge类来判断每次猜的数字是否正确,协程每次猜一个数字,通过co_yield发送给judge进行判断,猜中了结束,猜错了main函数调用承诺对象的句柄的resume方法恢复协程,重猜一个数字。

参考资料

协程——cppreference

协程初步:手把手教你写一个协程

posted @ 2025-02-16 16:13  cwtxx  阅读(57)  评论(0)    收藏  举报