最轻量级的C协程库:Protothreads

协程的好处不用再多说,作为与函数调用/返回相对的概念,它使我们思考问题的方式经历一场变革。现在我们关注的是C,由于C本身的特质,将协程引入其中将会是一 个挑战。无数先驱已经为这个目标抛了头颅洒了热血,于是我们有了libtask之类。而这里提到的,是一个堪称最轻量级的协程实现:Protothreads(主页:http://www.sics.se/~adam/pt/)。所谓最轻量级,就是说,功能已经不能再精简了,几乎就是原语级别的。——确实,这种最简带来了一些使用上的繁琐不便,但在打退堂鼓之前,先来看看它的优点吧:

  1. 不依赖任何库(包括C标准库和OS,是的,可以在bootloader里使用它),甚至本身都算不上个“库”,事实上整个实现都只有.h文件。
  2. 充上一条,.h文件共也只有5个而已,总共的有效行数也就100数量级(版本1.4)。
  3. 接着补充,那些行中大部分也都是宏定义,所以使用该库导致程序的膨胀基本可以忽略不计。
  4. 每个协程的内存开销只有一个指针那么大。

说实话,这种形式的所谓“库”的最佳使用方式,是去参考其源代码然后直接借鉴到自己的程序中。这么点代码就能实现协程的功能,其原理也就一层窗户纸。事实上Protothreads使用了两种方式来实现协程,你可以选择其中一种方式:

  1. 用switch语句来实现。
  2. 用GCC扩展语法来实现。

前者通用性好但低效,使用起来也有更多不便,后者相反。默认是前者,本人倾向于后者(后者MinGW也支持的),这归咎于用惯了GCC,而且后者从思想上确实更加简明,没有trick的意味。这里的原理叙述也以后者为主。

这个如洪水猛兽般的“扩展语法”,其实就是:可以把label地址保存到变量。label就是goto的那个label,就是那个人人喊打的goto。如下:

begin:
    printf("This is a message\n");
    /* goto begin; -- 我们本来应该这么用 */
    void *p = &&begin;
    goto *p;

&&不是取地址又取地址^_^而是扩展语法,这个运算符用于label,表示取其在代码段中的地址,就是说获得一个指针。指向代码段的指针,第一反应是函数指针,但这个不是,因为它并不指向一个函数的入口,而是指向其腹部。这种指针类型C中是不存在的,GCC也不想把事情搞大,整出个新数据类型来,于是用void *通吃了。这样这个值就可以当普通数据一样摆弄来摆弄去,最后靠goto *p,来从其他任何地方跳到这个地址来执行。

或许还记得,C的goto是不能跨越函数边界的,从理论角度这叫确保了单入单出的结构化编程,从底层实现角度,则保证了栈帧不混乱,即:如果goto到另一个函数的代码段中,但另一个函数的栈帧并没有准备好,栈顶还是当前函数的栈帧,那么目的函数在访问局部数据时候就会发生混乱。这种原来不可能发生的混乱,在这种扩展语法的支持下成为了可能。这是需要注意的一点,在使用扩展的goto语句的时候也要注意不要越过函数边界(当然,如果你BT到了解栈帧协议并试图手工建立栈帧的话,就当我没说^_^)。

Protothreads库对协程的实现,说来也简单,且看一个协程函数的示意:

int foo(struct pt *p) {
    PT_BEGIN(p);
    ……        /* 代码段1 */
    PT_YIELD(p);
    ……        /* 代码段2 */
    PT_END(p);
}

这个函数,在每次重入这个协程的时候都要被调用,靠这些PT_开头的宏,函数可以确定每次被调用时应该执行函数体的哪一部分。比如调用两次foo的话,第一次会执行代码段1,第二次则执行代码段2。原理如下:

结构体struct pt其实只有一个void *型成员,就是传说中那“一个指针的开销”,每个协程都有个对应的此物。该指针在初始化的时候被置NULL(由另一个宏PT_INIT在别处完成),在foo函数中,PT_BEGIN会检查这个指针,若是NULL,则表明是第一次启动该协程,什么也不做。接下来遇到了PT_YIELD,即协程挂起原语。此宏内部定义一个label,并立即将该label保存进pt结构体中。这样,此处可能有多种方式进入,一是顺序执行到此,二是从别处goto过来。这所谓别处,其实就在PT_BEGIN。如果它检查到pt不为空,则立即goto过去。现在PT_YIELD根据到达此处的方式做进一步判断,如果是自然执行到此,该挂起了,则立即reeturn出函数。否则,则是刚刚重入回来,继续执行下边的代码段2。这个判断是如何进行的?——靠一个标志位,PT_BEGIN每次被调用都首先置一个标志,而PT_YIELD则在label之前清除这个标志。这样,在label之后,PT_YIELD就可以据此判断,若标志没了,则是自然执行到此,若标志存在,则是从PT_BEGIN处goto过来的。——说穿了,就是setjmp的一个超轻量级版。

至于PT_END,其作用除了清除pt指针以外,主要是为了返回协程的状态。实际上PT_YIELD中的return也是带值的,之所以foo函数要声明为int,就是为了每次调用foo都能得到该协程当前的状态,是挂起了、结束了,还是中途退出了等等。
应该注意到了一点,就是既然每次重入协程都要重新调用foo函数,则说明foo函数中留不下任何状态,如果定义局部变量,则其内容都会丢失。嗯……这就是我指的“繁琐与不便”的主要所在吧,你需要让一切协程状态都以外部变量的形式存在,典型做法是封装成一个结构体,作为该函数的第二个参数。嗯,毕竟,C是接近底层的语言,让它自动背着你创建好多变量的副本,或者好多个协程局部的堆栈,还是不如你自己精确掌控对每块内存的使用,不是吗?毕竟不能用脚本语言的眼光来看C ^_^

现在,用这种方式创建了好多协程,那么接下来用一个简单的方式让它们运转起来,这个轮转调度简单得难以置信:

while (1) {
    foo1(p1);
    foo2(p2);
    ...
    foon(pn);
}

这就是调度器的主循环,只需要往复依次调用每个协程的入口函数即可。

以上叙述了Protothreads库的核心内容,实际上该库还包含了动态协程建立、协程间通信等设施,对于一个如此单薄的库来说,还是相当令人惊喜的。最后为了再次强调其单薄,在此列举一下其所有的头文件:

        lc-addrlabels.h        用GCC语法扩展实现的协程基础
        lc-switch.h            用switch语句实现的协程基础
        lc.h                  该文件存在的意义仅仅为了选择以上两者之一
        pt.h                 基于lc.h的协程设施的真正实现
        pt-sem.h               协程间通信(信号量)的实现
 
http://lych.yo2.cn/articles/%E6%9C%80%E8%BD%BB%E9%87%8F%E7%BA%A7%E7%9A%84c%E5%8D%8F%E7%A8%8B%E5%BA%93%EF%BC%9Aprotothreads.html
Protothread致命的弱点是block机制不能跨越函数,比如main->func1->func2...,则PT_WAIT_UNTIL只能放在main函数里面,不可以放在func1和func2里面,因为不论是switch case还是label都不能跨越函数。另外,函数中最好不要使用局部变量,出问题的时候多想想这里,是不是变量的初始化或赋值被switch case或goto给跳过了。
orchid是一个构建于强大的boost基础上的C ,类似于python下的gevent/eventlet,为用户提供基于协程的并发模型。 协程,顾名思义,协作式程序,其思想是,一系列互相依赖的协程间依次使用CPU,每次只有一个协程工作,而其他协程处于休眠状态。协程在控制离开时暂停执行,当控制再次进入时只能从离开的位置继续执行。 协程已经被证明是一种非常有用的程序组件,不仅被python、lua、ruby等脚本语言广泛采用,而且被新一代面向多核的编程语言如golang rust-lang等采用作为并发的基本单位。 协程可以被认为是一种用户空间线程,与传统的抢占式线程相比,有2个主要的优点: 与线程不同,协程是自己主动让出CPU,并交付他期望的下一个协程运行,而不是在任何时候都有可能被系统调度打断。因此协程的使用更加清晰易懂,并且多数情况下不需要锁机制。 与线程相比,协程的切换由程序控制,发生在用户空间而非内核空间,因此切换的代价非常的小。 green化 术语“green化”来自于python下著名的协程greenlet,指改造IO对象以能和协程配合。某种意义上,协程与线程的关系类似与线程与进程的关系,多个协程会在同一个线程的上下文之中运行。因此,当出现IO操作的时候,为了能够与协程相互配合,只阻塞当前协程而非整个线程,需要将io对象“green化”。目前orchid提供的green化的io对象包括: tcp socket(还不支持udp) descriptor(目前仅支持非文件类型文件描述符,如管道和标准输入/输出,文件类型的支持会在以后版本添加) timer (定时器) signal (信号) chan:协程间通信 chan这个概念引用自golang的chan。每个协程是一个独立的执行单元,为了能够方便协程之间的通信/同步,orchid提供了chan这种机制。chan本质上是一个阻塞消息队列,后面我们将看到,chan不仅可以用于同一个调度器上的协程之间的通信,而且可以用于不同调度器上的协程之间的通信。 多核 建议使用的scheduler per cpu的的模型来支持多核的机器,即为每个CPU核心分配一个调度器,有多少核心就创建多少个调度器。不同调度器的协程之间也可以通过chan来通信。协程应该被创建在哪个调度器里由用户自己决定。 进一步信息请阅读doc目录下tutorial。如果您发现任何bug或者有任何改进意见,请联系ioriiod0@gmail.com 标签:orchid
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值