有时候需要将一个同步的逻辑打断,然后在需要的时候再接着回来执行,就是将一个同步逻辑改成一个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的实现代码。