C++中的yield和reenter和fork

原链接:

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 1
foo2 101
foo2 3

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

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

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

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

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

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

很遗憾,这是不行的,这会有一个编译错误,Switch case is in 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实际被放在case 0:中,回想一下_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到达!

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

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

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。当然也可以和小麦探讨哦~


  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值