现在的位置: 首页 > 架构设计 > 正文

Coroutine的co_yield如何使用

2020年01月15日 架构设计 ⁄ 共 2282字 ⁄ 字号 评论关闭

  有时候需要将一个同步的逻辑打断,然后在需要的时候再接着回来执行,就是将一个同步逻辑改成一个lazy的producer。这种功能的解决方案一般是这样:如果是循环,则采用状态机来改写;如果是递归,则采用栈+状态机来改写。无论怎样,都不是最简单的方法,采用协程才是最简单的方法。这也是本文要介绍的co_yield的功能。

  举个例子,有一个简单的排序算法,它是用循环实现的:

  void Sort(vector& v)

  {

  for(int i = int(v.size()) - 1; i >= 0; --i)

  {

  for(int j = 0; j < i; ++j)   {   if(v[j] > v[j+1])

  {

  swap(v[j], v[j+1]);

  }

  }

  }

  }

  这本身是一个连续的逻辑,如果要把它的过程展示成如下所示的动画,直接改这个循环就不好做了。

  如果要做这个动画,需要将循环改成状态机来实现。循环中的变量只有i、j两个,将它们拿出来即可。状态机的代码我不放了。直接来看用coroutine怎么做。

  generator Sort(vector& v)

  {

  for(int i = int(v.size()) - 1; i >= 0; --i)

  {

  for(int j = 0; j < i; ++j)   {   co_yield j;   }   }   }   // 给generator类加上iterator的接口后的用法示例   auto s = Sort(my_vector);   auto it = s.begin();   if(it != s.end())   {   int j = *it; // 取得本次j的值   if(my_vector[j] > my_vector[j+1])

  {

  swap(my_vector[j], my_vector[j+1]);

  // 绘制动画,第j个和第j+1个进行交换

  // 绘制完成以后再调用++it获取下一个j的值

  }

  }

  上面的代码将两重循环改成了一个可以被打断、且可以在任意时候再接着执行的逻辑。每次拿取一个值,绘制动画,绘制完成以后调用获取下一个值。这样做成的动画还可以暂停,因为任何时候都可以再接着执行,不需要自己去写状态机的代码,这就是co_yield的威力。越是复杂的循环、局部变量越多的,改成状态机就越繁琐,用co_yield来做就更简单。

  和前文提到的task一样,generator本该是C++标准库提供的一个工具类,但是需要等到2023年才提供,所以目前需要自己写。其实generator的写法更简单,因为generato通常不需要实现自己的awaitable,直接用标准库提供的suspend_never/suspend_always就足够了。规则如下:

  co_yield xxx 会被转化为 co_await promise.yield_value(xxx)

  generator的实现很简单,也很灵活,根据不同的需求可以写出n个版本。比如说用c++17提供的std::optional可以实现成这样:

  template < typename T>

  struct generator {

  struct promise_type {

  suspend_never initial_suspend() { return {}; }

  suspend_always final_suspend() { return {}; }

  auto yield_value(T v) {

  val.emplace(std::move(v));

  return suspend_always();

  }

  auto get_return_object() { return generator(this); }

  void return_void() {}

  void unhandled_exception() { throw; }

  auto coro() { return coroutine_handle::from_promise(*this); }

  std::optional val;

  };

  generator(generator&& source) : p(std::exchange(source.p, nullptr)) {}

  explicit generator(promise_type* p) :p(p) {}

  ~generator() {

  if (p) p->coro().destroy();

  }

  // 为了实现range based for和iterator,加上这些代码

  struct EndSentinel{};

  auto end() { return EndSentinel(); }

  auto begin() {

  struct Iter {

  bool operator!=(EndSentinel) const { return !p->coro().done(); }

  void operator++() { p->coro().resume(); }

  T& operator*() const { return *p->val; }

  promise_type* p;

  };,

  return Iter{p};

  }

  promise_type* p;

  };

  如果是要将递归逻辑用co_yield来改,比如说要做一个汉诺塔动画,generator的实现要更麻烦一些,这也是stackless coroutine相比stackful coroutine的缺点。相关的代码我不写了,有兴趣的自己去看cppcoro库的recursive_generator的实现代码。

抱歉!评论已关闭.