协程的额外补充

看过了几个古老的有栈协程库实现,深入理解bthread的设计思想之后还是觉得不完美。决定学习一下最新的相关技术来补充思路。这里主要参考邓子峰大佬https://codesire-deng.github.io/2021/11/18/C-20-Coroutine/的开源框架

回顾一下无栈协程的实现

相较于有栈协程自己管理的函数运行栈空间(独立栈或者共享栈),无栈协程不需要对这些空间做管理,但是无栈协程需要一个frame(协程帧)保存该协程的运行状态。协程帧主要保存参数和局部变量的值,在让出执行权的时候能保存运行状态和执行流节点。像邓子峰的协程视频中使用fib_frame保存ab变量值还有state状态,每次重新进入fib根据state值跳转到对应的执行流。

c++20协程的相关理论参考bennyhuo大佬的文章https://www.bennyhuo.com/book/cpp-coroutines/01-intro.html

有栈协程的理解对于开发者来说只是定义个栈空间来运行函数,而无栈协程在c++中,使用和理解难度都大了不少。

先通过一个简单的示例了解生成器和promise对象是什么东西

#include <coroutine>
#include <iostream>
#include <optional>

// 定义一个生成器,它将产生整数序列
template <typename T>
struct Generator {
    struct promise_type {
        std::optional<T> value;

        Generator<T> get_return_object() {
            return { 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::suspend_always yield_value(T value) {
            this->value = value;
            return {};
        }

        void return_void() {}
    };

    std::coroutine_handle<promise_type> handle;

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

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

    bool next(T& value) {
        if (!handle || handle.done()) {
            return false;
        }

        handle.resume();

        if (handle.promise().value) {
            value = handle.promise().value.value();
            handle.promise().value.reset();
            return true;
        }

        return false;
    }
};

// 使用生成器的协程函数
Generator<int> countUpTo(int n) {
    for (int i = 0; i < n; ++i) {
        co_yield i; // 产生下一个值
    }
}

int main() {
    Generator<int> gen = countUpTo(5); // 创建一个生成器,产生0到4的整数
    int value;

    while (gen.next(value)) { // 循环获取生成器产生的值
        std::cout << "Generated value: " << value << std::endl;
    }

    return 0;
}

直接看这个程序的执行流

Generator gen = countUpTo(5);

调用countUpTo,实例化Generator。分配必要的内存来存储协程的状态,创建一个与这个协程关联的 promise_type 对象。promise_type 是协程框架的一部分,它管理着协程的生命周期,包括挂起、恢复和销毁。

countUpTo 函数通过调用其 promise_type 的 get_return_object 方法来获取一个表示该协程的句柄(在这个例子中是 Generator 类型的对象)。这个句柄允许外部代码控制协程的执行,比如恢复挂起的协程或检查协程是否已经完成。

协程句柄赋值给gen变量

在这一步中,因为initial_suspend()设置的返回值是std::suspend_always ,countUpTo 函数的函数体并未开始执行(除了与协程初始化相关的部分)。协程的实际执行会在稍后通过调用 gen.next(value) 并进而调用 handle.resume() 时开始。

gen.next(value)

bool next(T& value) {
if (!handle || handle.done()) {
return false;
}

handle.resume();

if (handle.promise().value) {
  value = handle.promise().value.value();
  handle.promise().value.reset();
  return true;
}

return false;

}

检查协程状态

handle.resume();恢复协程

协程执行到co_yield i会挂起,并将值存储在promise对象的value中,执行流回到resume的调用方

引用的value被赋值promise的value值

handle.promise().value.reset();调用 std::optional 类型的 reset() 方法。这个方法将 optional 对象重置为不包含值的状态,即使其变为“空”状态。在这个上下文中,它用于在每次从生成器中取出值之后清空 value,以便为下一次 yield_value 做准备。(

在协程生成器的设计中,清空 promise 中的 value 而不是在下次存储时直接覆盖,有几个重要的原因:

  1. 状态明确性:通过显式地清空 value,生成器的状态变得更加明确。在每次 yield 之后,value 被重置为一个明确的不包含任何值的状态(对于 std::optional 来说就是“空”状态)。这有助于减少错误和混淆,因为代码的读者可以清楚地看到 value 在每次使用之后都被重置。
  2. 防止旧值残留:如果不清空 value,而是直接在下次存储时覆盖,那么在某些情况下可能会出现旧值意外地被使用的情况。特别是在协程的执行流程变得复杂或者存在多个分支时,这种残留的旧值可能导致难以追踪的错误。
  3. 资源管理:虽然在这个特定的例子中 value 是一个简单的整数,但如果 value 是一个需要手动管理资源的复杂对象(比如动态分配的内存或文件句柄),那么及时清空它就显得尤为重要。通过清空 value,可以确保相关资源在不再需要时被正确释放,从而避免资源泄漏。
  4. 逻辑清晰性:从逻辑上讲,每次从生成器中获取一个值后,该值就已经被“消费”了。因此,将 value 重置为未初始化状态符合这种“消费”后的直观期望。这有助于保持代码的清晰性和可读性。
  5. 异常安全:在协程中处理异常时,确保状态的一致性是非常重要的。如果在 yield_value 之后和下次 resume 之前发生异常,那么未清空的 value 可能会处于一个不一致或不可预测的状态。通过显式清空 value,可以减少这种状态不一致导致的问题。

总的来说,清空 promise 的 value 是一种良好的编程实践,它有助于确保生成器的正确性和健壮性。这种做法遵循了“初始化后立即使用,使用后立即清理”的原则,从而减少了潜在的错误和资源泄漏风险。

​ 虽然不清空value,大多数情况下都程序都能正常运行,但是像if (handle.promise().value)这种判断就会失效,为了避免这些情况导致可能存在的bug,还是建议规范执行。

当协程执行完成后

for循环条件不满足 协程执行完成到 void return_void(){},std::suspend_always final_suspend() noexcept

  1. for循环条件不满足:当驱动协程的for循环发现其条件不再满足时,它会停止迭代。这意味着没有更多的外部请求来恢复(resume)协程的执行。
  2. 协程执行完成:如果此时协程已经到达了其逻辑终点(例如,执行到了协程函数的末尾,或者显式地通过某种方式标记为完成),那么协程被认为是执行完成的。
  3. 执行流到void return_void(){}:这通常表示协程的返回语句被执行。在C++协程中,你可以有一个特殊的返回对象或函数,用于处理协程完成时的清理工作。return_void可能是一个表示协程正常结束的辅助函数。
  4. 回到generator协程:这里的“generator协程”可能指的是协程的具体实现或包装器。在协程完成执行后,控制流通常会返回到这个层面,以便进行必要的后处理。
  5. 到std::suspend_always final_suspend() noexcept:这是C++协程中的一个关键部分。final_suspend是协程承诺对象(promise object)的一个成员函数,它在协程即将完成时被调用。std::suspend_always是一个指示器,告诉运行时系统总是挂起协程(在这种情况下,实际上是表示协程已经完成,并且不会再被恢复)。
  6. 返回调用resume方:一旦final_suspend被调用并且返回了std::suspend_always,控制流将返回到最初调用resume的地方。这通常意味着协程的执行已经完全结束,并且调用者可以继续执行后续的代码。
struct promise_type {
        std::optional<T> value;
        
        Generator<T> get_return_object() {
            return {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::suspend_always yield_value(T value) {
            this->value = value;
            return {};
        }
        
        void return_void() {}
    };

get_return_object():

  • 这个函数返回一个表示协程的对象,通常是一个封装了std::coroutine_handle的自定义类型。这个对象允许外部代码与协程交互,比如恢复(resume)协程或检查其状态。

initial_suspend():

  • 这个函数在协程开始时被调用,决定了协程是立即执行还是挂起。返回std::suspend_always会导致协程在开始时挂起,而返回std::suspend_never则允许协程立即执行。在你的例子中,协程会立即执行。

final_suspend():

  • 当协程执行到终点时(例如,执行了co_return语句或函数正常结束),会调用这个函数。它决定了协程在结束前是否挂起。通常,这个函数返回std::suspend_always,表示协程已经结束。

unhandled_exception():

  • 如果协程中抛出了一个未被捕获的异常,这个函数会被调用。在你的代码中,它调用了std::terminate(),这会导致程序终止。这是一种简单的异常处理策略,但在实际应用中,你可能需要更复杂的错误处理逻辑。

yield_value(T value):

  • 当协程执行co_yield语句时,会调用这个函数。它允许你将一个值“产出”(yield)给协程的调用者,并且通常会保存这个值以供外部访问。在你的代码中,产出的值被保存在value成员变量中。

return_void():

  • 当协程通过co_return;(没有返回值的情况)结束时,会调用这个函数。它通常用于执行一些清理工作。在你的代码中,这个函数是空的,因为没有特定的清理工作需要执行。

这些函数共同定义了协程的行为和生命周期。虽然它们不是全部都必须的,但缺少某些函数可能会导致编译错误或运行时问题,因为编译器和运行时系统期望这些函数存在并正确实现。在自定义协程时,你应该根据你的具体需求来实现这些函数。

这些函数的名称和返回值类型在C++协程的上下文中是由C++标准规定的。编译器期望在协程的promise_type中找到这些特定名称的函数,并且它们必须具有与标准相符的签名。

C++20引入了协程作为语言的一部分,并定义了一套用于支持协程的底层机制。promise_type是这套机制中的关键组件,它充当了协程与其外部环境之间的桥梁。编译器使用这些函数来控制协程的执行流程,处理异常,以及管理协程的状态。

具体来说:

  • 函数名称(如get_return_object, initial_suspend, final_suspend, unhandled_exception, yield_value, return_void)是固定的,编译器会查找这些名称的函数。
  • 函数的返回值类型也是由标准规定的。例如,initial_suspend和final_suspend必须返回一个表示挂起操作的类型,通常是std::suspend_always、std::suspend_never或std::suspend_if。yield_value函数返回一个表示挂起的类型,通常也是std::suspend_always,但它还接受一个参数,该参数的类型与协程产生的值的类型相同。
  • 函数的参数列表(对于接受参数的函数,如yield_value)同样是固定的。

这些规定确保了编译器能够正确地与协程的promise_type交互,从而实现协程的语义。如果你正在实现自己的协程类型或库,你需要遵循这些规定来确保你的协程能够与C++的协程机制兼容。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值