勤快学

协程(Coroutine)

技术随笔  标签:Coroutine    发布于:2016年11月29日

协程(Coroutine)

协程一般用来进行非抢占式的多任务(nonpreemptive multitasking),可以有多个入口点,通过yield来释放、切换到其他协程,以及痛过resume等方法进入上次的切换点。

非抢占式多任务处理(进程调度)是一种进程调度的方式,与其相对的是抢占式(进程调度);非抢占式让原来正在运行的进程继续运行,直至该进程完成或发生某种事件(如I/O请求),才主动放弃处理机;非抢占式需要任务自身主动释放所有权,别的任务才可以执行。

抢占式多任务处理(preemptive multitasking)是将同时进行的各项任务(task),依照重要程度来排定优先顺序。在抢占式多任务系统中,操作系统(operating system)必须具有从任何一个运行的程序上取走控制权和使另一个程序获得控制权的能力。Windows的进程和线程调度,都属于抢占式多任务处理。

下面是一个协程的简单示例:

void foo()
{
   yield std::cout<<"a";  //1、执行到此处(yield)跳出,下次进入从这之后执行

   yield std::cout<<"b";  

   yield std::cout<<"c";  
}

void bar()
{
   yield std::cout<<"1";  //1、执行到此处跳出,下次进入从这之后执行

   yield std::cout<<"2";  

   yield std::cout<<"3";  
}

int main()
{
   for(int i = 0; i < 3;i++)
      foo();
      bar();
}

output:
 a 1 b 2 c 3

调用序列如下:



1、Coroutine vs Subroutine

子例程的起始处是唯一的入口点,一旦结束,就不可以重入,平时调用的函数函数对象等就属于这一类。

而协程是可以切换和重入的。

2、stackful vs stackless

协程一般有两类实现,一种是stackless,一种是stackful。

stackless类型不保存调用栈以及寄存器等信息,不属于真正的重入,因此一些局部变量都是无法使用的。

stackful是真正的基于栈的重入,脚本语言引擎实现此特性很简单,C/C++也有实现,可能要在汇编层面做一些工作了。比如说boost.coroutine2就是利用boost.context保存了上下文信息,实现了stackful的协程。

stackless的优点是效率相对比较高,缺点是功能受限,比如无法使用局部变量,无法真正跳转到上次跳出的位置。

下面是一个boost.asio的一个stackless协程实现,可以比较直观得看出两者的区别。

3、boost.asio stackless coroutine

#include <iostream>
#include <boost/asio/yield.hpp>

int foo(boost::asio::coroutine &ct) {
   std::cout << "before reenter" << std::endl;

   reenter(ct) {//可重入区域
       std::cout << "before yield1" << std::endl;
      //定义跳转点1
       yield std::cout << "yield1" << std::endl; 

       std::cout << "before yield2" << std::endl;
       yield return 1;//定义跳转点2
   }
   std::cout << "after reenter" << std::endl;
   return 2;
}

int main(int argc, char *argv[]) {
   boost::asio::coroutine ct;
   while (!ct.is_complete()) {
       int ret = foo(ct);
       std::cout << "return:" << ret << std::endl;
   }
   return 0;
}

reenter和yield是两个宏,痛过switch和jump来控制跳转。 
使用行号LINE来控制重入点位置。

#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:

#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:

# define BOOST_ASIO_CORO_YIELD BOOST_ASIO_CORO_YIELD_IMPL(__LINE__)
# define BOOST_ASIO_CORO_FORK BOOST_ASIO_CORO_FORK_IMPL(__LINE__)

在boost.asio中有一个http server的例子,就用了无堆栈的协程。

在传统的异步程序中,我们可能需要写多个回调,而且回调之前看不出调用顺序。

void handle_accept(){}
void handle_read(){}

而使用协程,我们就可以写出这样的代码了(伪码):

void  coro_handler(){
   //首次进入,投递接受连接请求
   yield aync_accept_connect();
   
  //第二次进入,说明连接已建立,可以发送接收数据请求
  //循环接受,直到接收完整报文
  bool is_recv_completed = false;
   do{
       yield aync_receive_some_data();
       is_recv_completed = handle_data();
   }while(!is_recv_completed)

   send_response();

   close_connection();
}

对比异步回调好处有两个:

  1. 没有太多的回调函数,可以缩略为一个,接受连接、接收数据、发送数据、关闭连接,比较直观。

  2. 函数调用栈清晰。

另外,使用非抢占式的多任务,由于没有内核态的切换,因此效率也比较高。

代码在这里 
Httpserver4

4、stackful coroutine

未完待续。


上一篇:GAMS Pureformance Challenge Ceremony 纯境挑战赛颁奖仪式

下一篇:《纽约时报》:人工智能“之父”之争,被忽视的Jürgen Schmidhuber