0
点赞
收藏
分享

微信扫一扫

探究C++20协程(2)——取值、传值、销毁与序列生成器实现

小编 04-13 08:30 阅读 1

序列生成器是一个非常经典的协程应用场景,尤其是在需要惰性生成数据或处理潜在无限的数据流时。

序列生成器概念:序列生成器允许程序按需生成序列中的下一个元素,而不是一次性计算整个序列。这种方式可以节省内存,并允许处理无限或未知长度的数据序列。

实现目标

简单的说,序列生成器通常的实现就是在一个协程内部通过某种方式向外部传一个值出去,并且将自己挂起,外部调用者则可以获取到这个值,并且在后续继续恢复执行序列生成器来获取下一个值。

显然,挂起和向外部传值的任务就需要通过 co_await 来完成了,外部获取值的任务就要通过协程的返回值来完成。

由此程序大致框架如下:

Generator sequence() {
  int i = 0;
  while (true) {
    co_await i++;
  }
}

int main() {
  auto generator = sequence();
  for (int i = 0; i < 10; ++i) {
    std::cout << generator.next() << std::endl;
  }
}

在generator 有个 next 函数,调用它时需要想办法让协程恢复执行,并将下一个值传出来。

调用者获取值

generator 的类型就是我们即将实现的序列生成器类型 Generator,结合上一篇文章当中对于协程返回值类型的介绍,我们先大致给出它的定义:

struct Generator {
  struct promise_type {
    // 开始执行时直接挂起等待外部调用 resume 获取下一个值
    std::suspend_always initial_suspend() { return {}; };
    // 执行结束后不需要挂起
    std::suspend_never final_suspend() noexcept { return {}; }
    // 为了简单,我们认为序列生成器当中不会抛出异常,这里不做任何处理
    void unhandled_exception() { }
    // 构造协程的返回值类型
    Generator get_return_object() {
      return Generator{};
    }
    // 没有返回值
    void return_void() { }
  };

  int next() {
    //这里需要恢复线程
  }
};

想要在 Generator 当中 resume 协程的话,需要拿到 coroutine_handle。

promise_type 是连接协程内外的桥梁,标准库提供了一个通过 promise_type 的对象的地址获取 coroutine_handle 的函数,它实际上是 coroutine_handle 的一个静态函数:

//vs2022
struct coroutine_handle {
    constexpr coroutine_handle() noexcept = default;
    constexpr coroutine_handle(nullptr_t) noexcept {}

    _NODISCARD static coroutine_handle from_promise(_Promise& _Prom) noexcept { 
        // strengthened
        const auto _Prom_ptr  = const_cast<void*>(static_cast<const volatile void*>(_STD addressof(_Prom)));
        const auto _Frame_ptr = __builtin_coro_promise(_Prom_ptr, 0, true);
        coroutine_handle _Result;
        _Result._Ptr = _Frame_ptr;
        return _Result;
    }

这样只需要在 get_return_object 函数调用时,先获取 coroutine_handle,然后再传给即将构造出来的 Generator 即可。

协程内部挂起并传值

观察一下最终实现的效果:

Generator sequence() {
  int i = 0;
  while (true) {
    co_await i++;
  }
}

特别需要注意的是 co_await i++; 其 后面的是一个整型值,而不是在前面的文章当中提到的满足等待体(awaiter)条件的类型,这种情况下该怎么办呢?

实际上,对于 co_await 表达式当中 expr 的处理,C++ 有一套完善的流程:

  • 如果 promise_type 当中定义了 await_transform 函数,那么先通过 promise.await_transform(expr) 来对 expr 做一次转换,得到的对象称为 awaitable;否则 awaitable 就是 expr 本身。

  • 接下来使用 awaitable 对象来获取等待体(awaiter)。如果 awaitable 对象有 operator co_await 运算符重载,那么等待体就是 operator co_await(awaitable),否则等待体就是 awaitable 对象本身。

那么只需要为数据类型实现一个 operator co_await 的运算符重载即可。

struct Generator {
  struct promise_type {
    int value;
    // 传值的同时要挂起,值存入 value 当中
    std::suspend_always await_transform(int value) {
      this->value = value;
      return {};
    }
  };

  std::coroutine_handle<promise_type> handle;

  int next() {
    handle.resume();

  // 外部调用者或者恢复者可以通过读取 value
    return handle.promise().value;
  }
};

定义了 await_transform 函数之后,co_await expr 就相当于 co_await promise.await_transform(expr) 了。

协程的销毁

问题1:无法确定是否存在下一个元素

当外部调用者或者恢复者试图调用 next 来获取下一个元素的时候,它其实并不知道能不能真的得到一个结果。

为了解决这个问题,我们需要增加一个 has_next 函数,用来判断是否还有新的值传出来,has_next 函数调用的时候有两种情况:

  • 已经有一个值传出来了,还没有被外部消费
  • 还没有现成的值可以用,需要尝试恢复执行协程来看看还有没有下一个值传出来
struct Generator {
  bool has_next() {
    // 协程已经执行完成
    if (handle.done()) {
      return false;
    }

    // 协程还没有执行完成,并且下一个值还没有准备好
    if (!handle.promise().is_ready) {
      handle.resume();
    }

    if (handle.done()) {
      // 恢复执行之后协程执行完,这时候必然没有通过 co_await 传出值来
      return false;
    } else {
      return true;
    }
  }

  int next() {
    if (has_next()) {
      // 此时一定有值,is_ready 为 true 
      // 消费当前的值,重置 is_ready 为 false
      handle.promise().is_ready = false;
      return handle.promise().value;
    }
    throw ExhaustedException();
  }
};

问题2:协程状态的销毁比 Generator 对象的销毁更早

协程的状态在协程体执行完之后就会销毁,除非协程挂起在 final_suspend 调用时。为了让协程的状态的生成周期与 Generator 一致(在Generator可能会使用导协程状态),我们必须将协程的销毁交给 Generator 来处理:

struct Generator {

  class ExhaustedException: std::exception { };

  struct promise_type {
    // 总是挂起,让 Generator 来销毁
    std::suspend_always final_suspend() noexcept { return {}; }

  };


  ~Generator() {
    // 销毁协程
    handle.destroy();
  }
};

问题3:复制对象导致协程被销毁

在 Generator 的析构函数当中销毁协程,这本身没有什么问题。但如果把 Generator 对象做一下复制:

Generator returns_generator() {
  auto g = sequence();
  if (g.has_next()) {
    std::cout << g.next() << std::endl;
  }
  return g;
}

由于把 g 当做返回值返回了,这时候 g 这个对象就发生了一次复制,然后临时对象被销毁,协程也就没了,再调用直接dump。

为了解决这个问题,需要妥善地处理 Generator 的复制构造器:

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

  Generator(Generator &&generator) noexcept
      : handle(std::exchange(generator.handle, {})) {}

  Generator(Generator &) = delete;
  Generator &operator=(Generator &) = delete;

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

只提供了右值复制构造器,对于左值复制构造器,我们直接删除掉以禁止使用。原因也很简单,对于每一个协程实例,都有且仅能有一个 Generator 实例与之对应,因此我们只支持移动对象,而不支持复制对象。

序列生成器完整实现

#include <coroutine>
#include <exception>
#include <iostream>
#include <thread>

struct Generator {
    class ExhaustedException : std::exception { };
    struct promise_type {
        int value;
        bool is_ready = false;
        std::suspend_always initial_suspend() { return {}; };
        std::suspend_always final_suspend() noexcept { return {}; }
        std::suspend_always await_transform(int value) {
            this->value = value;
            is_ready = true;
            return {};
        }
        void unhandled_exception() {}
        Generator get_return_object() {
            return Generator{ std::coroutine_handle<promise_type>::from_promise(*this) };
        }
        void return_void() { }
    };
    std::coroutine_handle<promise_type> handle;
    bool has_next() {
        if (handle.done()) {
            return false;
        }

        if (!handle.promise().is_ready) {
            handle.resume();//让协程恢复执行
        }

        if (handle.done()) {
            return false;
        }
        else {
            return true;
        }
    }

    int next() {
        if (has_next()) {
            handle.promise().is_ready = false;
            return handle.promise().value;
        }
        throw ExhaustedException();
    }

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

    Generator(Generator&& generator) noexcept
        : handle(std::exchange(generator.handle, {})) {}

    Generator(Generator&) = delete;
    Generator& operator=(Generator&) = delete;

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

Generator sequence() {
    int i = 0;
    while (i < 5) {
        co_await i++;
    }
}

Generator returns_generator() {
    auto g = sequence();
    if (g.has_next()) {
        std::cout << g.next() << std::endl;
    }
    return g;
}

int main() {
    auto generator = returns_generator();
    for (int i = 0; i < 15; ++i) {
        if (generator.has_next()) {
            std::cout << generator.next() << std::endl;
        }
        else {
            break;
        }
    }
    return 0;
}

使用 co_yield

C++ 当中的 co_yield expr 等价于 co_await promise.yield_value(expr),我们只需要将前面例子当中的 await_transform 函数替换成 yield_value 就可以使用 co_yield 来传值了:

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

通常情况下使用 co_await 更多的关注点在挂起自己,等待别人上,而使用 co_yield 则是挂起自己传值出去。

使用序列生成器生成斐波那契数列

Generator fibonacci() {
    co_await 0; // fib(0)
    co_await 1; // fib(1)

    int a = 0;
    int b = 1;
    while (true) {
        co_await (a + b); // fib(N), N > 1
        b = a + b;
        a = b - a;
    }
}

int main() {
    auto generator = fibonacci();
    for (int i = 0; i < 15; ++i) {
        if (generator.has_next()) {
            std::cout << generator.next() << std::endl;
        }
        else {
            break;
        }
    }
    return 0;
}

fibonacci():通过连续的 co_await 表达式生成斐波那契数列的值。首先固定地生成 0 和 1,然后进入循环,不断计算后续数值并通过 co_await 暂停和恢复协程,以生成数列。

协程的启动和恢复是通过 Generator::has_next 和 Generator::next 中的 handle.resume() 来控制的。

每次 co_await 在 fibonacci 中被调用时,协程暂停,并在 await_transform 中处理新的值。

当 co_await 后的表达式执行完毕后,协程在 await_transform 返回的挂起点恢复。

举报

相关推荐

0 条评论