C++中的yield和reenter和fork

来源:互联网 发布:大胃王密子君淘宝店 编辑:程序博客网 时间:2024/06/06 02:58

原链接:

http://ju.outofmemory.cn/entry/57710


各位看官,您没有看错,C++是可以有yield和fork的,这个主题小麦很早以前就打算写,只是一直没有一个契机给我这个动力。前段日子,小麦帮朋友处理一个用单线程模拟多线程的活儿的时候,再次想到了这个事情,决定写一下,也算是自己的一个回顾。本文其实也可以叫做boost::asio::coroutine原理解析。。。

当然,C++本身是没有yield这个关键字的,这个关键字的意义可以参考C#对其的定义,就是跳出当前的执行过程,下次调用这个过程的时候,直接从yield处开始执行。例如

int foo(){    yield return 1;    yield return 2;    yield return 3;}

连续调用foo函数3次,分别得到了1,2,3这三个数,这就是yield的基本语义,而fork的意思是保存当前上下文,跳转到目标函数,在目标函数执行完成之后,回到当前位置,例如

std::cout<<"line 1"<<std::endl;fork foo()

从上面的例子可以看出,yield和fork的作用就相当于用单线程模拟了多线程的语义。这个语义正是常说的coroutine,即协程。通常来说,协程的实现是保存当前上下文,然后在线程内不断切换上下文,这种情况也叫做stackfull coroutine.本文要描述的是不保存上下文的情况,即stackless coroutine。

怎么用

如果各位看过C#中关于 yield的实现的话,可能会有一些印象,C#把yield语句翻译成了一个状态机,利用状态机记录每次进入函数时应该从何处开始执行。然而在C++中,失去了编译器的支持,想实现这个语义还是颇为困难的。下面的实现是boost asio的作者给出的实现,其逆天程度之高,以致于这个实现最开始并没有出现在boost asio的官方文档中,而是在boost asio的example中静静的躺着,直到最近的boost 1.54,这个技术才正式成为boost asio的一部分,

简而言之,这个实现是用宏实现的,请各位忘记C++的程序跳转,记住C#的yield,我们来看个例子

#include <boost/asio/yield.hpp>#include <boost/asio/coroutine.hpp>#include <iostream>boost::asio::coroutine c;void foo(int i){    reenter(c)    {        yield std::cout<<"foo1 "<<i<<std::endl;        fork foo(100);        yield std::cout<<"foo2 "<< i+1<<std::endl;    }}int main(){    foo(1);    foo(2);    foo(3);    return 0;}

这个程序的输出是。。。

foo1 1foo2 101foo2 3

这个咋看之下有点难以理解,我们先忽略具体的细节,解释一下这是怎么回事。

首先,我们需要声明一个corountine,然后将需要重复进入的代码用reenter包括起来。第一次调用foo的时候,代码执行到第一个yield,此时,foo直接返回。

第二次调用的时候,程序直接执行上一次yield之后的代码,即fork,此时,程序调用foo,需要注意的是,被调用的fork不再执行第一个yield,而是直接从当前语句开始执行,于是得到输出foo2101,返回之后,程序调用fork之后的yield,得到输出foo23

第三次调用的时候,由于不再有任何未执行的yield,因此不再产生任何输出。

很自然的,我们会想像C#中的yield一样使用,类似这样

int nums(){    reenter(c)    {        for(int i = 0; i < 10; i++)        {            yield return i;        }    }}

很遗憾,这是不行的,这会有一个编译错误,Switchcase isin protected scope,这显然让人觉得不可思议,联想到我们写的程序本身就不太像正常的C++代码,我们有必要详细了解一下到底发生了什么。

内部实现

首先,我们先从能看见的类开始,coroutine

class coroutine{public:    /// Constructs a coroutine in its initial state.    coroutine() : value_(0) {}    /// Returns true if the coroutine is the child of a fork.    bool is_child() const { return value_ < 0; }    /// Returns true if the coroutine is the parent of a fork.    bool is_parent() const { return !is_child(); }    /// Returns true if the coroutine has reached its terminal state.    bool is_complete() const { return value_ == -1; }private:    friend class detail::coroutine_ref;    int value_;};namespace detail {class coroutine_ref{public:    coroutine_ref(coroutine& c) : value_(c.value_), modified_(false) {}    coroutine_ref(coroutine* c) : value_(c->value_), modified_(false) {}    ~coroutine_ref() { if (!modified_) value_ = -1; }    operator int() const { return value_; }    int& operator=(int v) { modified_ = true; return value_ = v; }private:    void operator=(const coroutine_ref&);    int& value_;    bool modified_;};} // namespace detail

上面这两个类,都很简单,从类名就可以看出来这是啥意思,略过不提。重要的是如何用宏实现reenter、yield、fork。

reenter的定义是一个宏,名字有重定义,如下

#define BOOST_ASIO_CORO_REENTER(c) \    switch (::boost::asio::detail::coroutine_ref _coro_value = c) \    case -1: if (_coro_value) \    { \        goto terminate_coroutine; \    terminate_coroutine: \        _coro_value = -1; \        goto bail_out_of_coroutine; \    bail_out_of_coroutine: \        break; \    } \    else case 0:

回想一下,reenter的用法是

reenter(coroutine){ your_code...;}

因此your_code实际被放在case0:中,回想一下_coro_value最初的初始化值是0,因此,第一次确实执行了your_code

yield的定义同样是个宏,定义如下

#define BOOST_ASIO_CORO_YIELD_IMPL(n) \    for (_coro_value = (n);;) \        if (_coro_value == 0) \        { \            case (n): ; \            break; \        } \        else \            switch (_coro_value ? 0 : 1) \                for (;;) \                    case -1: if (_coro_value) \                        goto terminate_coroutine; \                        else for (;;) \                            case 1: if (_coro_value) \                            goto bail_out_of_coroutine; \                                else case 0:

其中n的定义为__COUNTER___。

看到这个宏,估计看官和我最初反应差不多,深切的怀疑这个代码的合法性,因为有这样一种混乱的用法

switch(n){    for(...)        case 1: if(...) }

这个事实上就是将case 1看作普通的类似goto使用的代码标签,也就是说可以通过switch到达,也可以通过for到达!

这里用户的代码仍然在case0中出现,当执行这段代码是,_coro_value的值首先被设置成一个程序标识用的整数n, 在最内层的swithc-case中,_coro_value的值为n,因此,程序跳转到case0,首先执行用户的代码,用户代码执行完后,返回执行for循环,此时执行对应的if语句,即goto bail_out_of_coroutine,这个定义在之前的reenter的宏中,此时将直接break,返回整个reenter的最外层。

再次调用函数时,由于_coro_value的值仍然为n,则直接调用case0,直接执行后续的语句。

fork的实现如下,这个小麦就不再具体解释,各位看官请自行理解,比较简单。

#define BOOST_ASIO_CORO_FORK_IMPL(n) \    for (_coro_value = -(n);; _coro_value = (n)) \        if (_coro_value == (n)) \        { \            case -(n): ; \            break; \        } \        else

至此,我们就理解了前面的程序为什么会出现一个编译错误,因为大量的使用了switch-case,因此程序的层次很重要,不能在reenter中随意修改 yield和fork的层次,否则会导致switch-case不匹配,从而出现编译错误!!

总结

上述的用法并不是唯一的用法,各位还可以继承boost::asio::coroutine,而不是定义变量,更多用法可以参考boost::asio的example。当然也可以和小麦探讨哦~


0 0
原创粉丝点击