协程库st(state threads library)原理解析

协程库state threads library(以下简称st)是一个基于setjmp/longjmp实现的C语言版用户线程库或协程库(user level thread)。

这里有一个基本的协程例子 http://www.csl.mtu.edu/cs4411.ck/www/NOTES/non-local-goto/coroutine.html, 可以了解setjmp和longjmp的基本用法。如还有不懂,请自行查阅其他资料。本文主要关注st基于setjmp和longjmp的实现原理及其程序结构。

 

st基本介绍 http://state-threads.sourceforge.net/docs/st.html

从中可以看出,IA(Internet Application)架构演化历史:

1.多进程MP

以Apache为代表的web server。创建进程服务新用户,开销过高。调度单位是进程。

2.多线程MT

创建新线程服务新用户,线程间上下文切换,锁竞争等等,增加了额外开销。调度单位是线程。

3.事件驱动状态机EDSM

基于IO复用机制,实现大量并发请求的处理。但程序是一体的,并不是基于线程,新程序需要从头开始。该模式下主要使用回调和状态参数来进行上下文切换,实际上是以一种非常艰难和痛苦的方式实现了类似线程和栈的思想。ESDM最大的问题是其“将线性思路分解成大量的回调所固有的复杂性”,导致程序难以实现,扩展和维护。

传统EDSM程序架构:

4.协程

调度单位减小到函数,上下文切换不需要内核参与,不存在系统调用。上下文切换开销降到最低,系统调用降到最低,没有锁竞争,没有信号处理。保留了程序对请求的线性处理逻辑,提高了程序的开发效率,可扩展性和可维护性。

基于st的ESDM程序模型:

 

基于setjmp和longjmp实现协程库基本步骤(下述线程指用户线程):

1.需要用jmpbuf变量保存每一个线程的运行时环境,称为线程上下文context。

2.为每个线程分配(malloc/mmap)一个stack,用于该线程运行时栈,该stack完全等效于普通系统线程的函数调用栈。该stack地址是在线程初始化时设置,所以不需要考虑setjmp时保存线程的栈上frames数据的问题。

3.通过调用setjmp初始化线程运行时上下文,将context数据存放到jmpbuf结构中。然后修改其中的栈指针sp指向上一步分配的stack。根据当前系统栈的增长方向,将sp设置为stack的最低或最高地址。

4.线程退出时,需要返回到一个安全的系统位置。即,需要有一个主线程main thread或idle thread来作为其他线程最终的退出跳转地址。需要为主线程保存一个jmpbuf。

5.设置过main thread的jmpbuf后,需要跳转到其他线程开始执行业务线程。

6.实现一个context交换函数,在多个线程之间进行跳转:保存自己的jmpbuf,longjmp到另一个线程的jmpbuf。

 

st基于setjmp和longjmp的具体实现:

线程初始化:

#define MD_INIT_CONTEXT(_thread, _sp, _main) \
  ST_BEGIN_MACRO                             \
  if (MD_SETJMP((_thread)->context))         \
    _main();                                 \
  MD_GET_SP(_thread) = (long) (_sp);         \
  ST_END_MACRO

很明显可以看到,setjmp(将jmpbuf存放到thread->context)之后,同时修改它的栈指针sp指向新分配的线程stack->sp地址。该sp指针用于该thread以后的栈frames数据存储。

线程切换:

#define _ST_SWITCH_CONTEXT(_thread)       \
    ST_BEGIN_MACRO                        \
    ST_SWITCH_OUT_CB(_thread);            \
    if (!MD_SETJMP((_thread)->context)) { \
      _st_vp_schedule();                  \
    }                                     \
    ST_DEBUG_ITERATE_THREADS();           \
    ST_SWITCH_IN_CB(_thread);             \
    ST_END_MACRO

其中主要时MD_SETJMP保存当前context,然后调用_st_vp_schedule()从_ST_RUNQ上取第一个可运行的thread,并调用_ST_RESTORE_CONTEXT将该thread恢复运行。

线程恢复:

#define _ST_RESTORE_CONTEXT(_thread)   \
    ST_BEGIN_MACRO                     \
    _ST_SET_CURRENT_THREAD(_thread);   \
    MD_LONGJMP((_thread)->context, 1); \
    ST_END_MACRO

起始线程primordial thread和休眠线程idle thread:

/*
 * Initialize this Virtual Processor
 */
int st_init(void)
{
  _st_thread_t *thread;

  if (_st_active_count) {
    /* Already initialized */
    return 0;
  }

  /* We can ignore return value here */
  st_set_eventsys(ST_EVENTSYS_DEFAULT);

  if (_st_io_init() < 0)
    return -1;

  memset(&_st_this_vp, 0, sizeof(_st_vp_t));

  ST_INIT_CLIST(&_ST_RUNQ);
  ST_INIT_CLIST(&_ST_IOQ);
  ST_INIT_CLIST(&_ST_ZOMBIEQ);
#ifdef DEBUG
  ST_INIT_CLIST(&_ST_THREADQ);
#endif

  if ((*_st_eventsys->init)() < 0)
    return -1;

  _st_this_vp.pagesize = getpagesize();
  _st_this_vp.last_clock = st_utime();

  /*
   * Create idle thread
   */
  _st_this_vp.idle_thread = st_thread_create(_st_idle_thread_start,
                         NULL, 0, 0);
  if (!_st_this_vp.idle_thread)
    return -1;
  _st_this_vp.idle_thread->flags = _ST_FL_IDLE_THREAD;
  _st_active_count--;
  _ST_DEL_RUNQ(_st_this_vp.idle_thread);

  /*
   * Initialize primordial thread
   */
  thread = (_st_thread_t *) calloc(1, sizeof(_st_thread_t) +
                   (ST_KEYS_MAX * sizeof(void *)));
  if (!thread)
    return -1;
  thread->private_data = (void **) (thread + 1);
  thread->state = _ST_ST_RUNNING;
  thread->flags = _ST_FL_PRIMORDIAL;
  _ST_SET_CURRENT_THREAD(thread);
  _st_active_count++;
#ifdef DEBUG
  _ST_ADD_THREADQ(thread);
#endif

  return 0;
}

在st_init里面,创建primordial thread和idle thread。primordial thread作为起始线程当有其他线程加入运行队列后从该线程切出,idle thread作为背景线程在没有可运行线程的时候执行io调度函数分发事件。

st_init里面调用st_thread_create并不会开始执行idle线程,创建其他线程也一样,只有在直接或间接调用_st_vp_schedule之后才会开始执行RUNQ上面的线程。_st_vp_schedule函数在_ST_SWITCH_CONTEXT中被调用。

 

st程序结构:

st底层基于event-driven select/poll/kqueue/epoll等IO复用机制。下面以epoll为例说明st底层事件管理机制。

st中有IOQ,ZOMBIEQ,RUNQ,SLEEPQ等几个队列,用来存储处于对应状态的threads。

  • RUNQ中存储的是可以被调度运行的threads,每次调用_st_vp_schedule即从该队列取出一个thread去运行。
  • IOQ存储处于IO等待状态的threads,当上层调用st_poll时,将该thread放入IOQ中;当底层epoll有IO事件到达时,将该thread从IOQ中移除,并放入RUNQ中。
  • 当thread退出时,放入ZOMBIEQ中。
  • 当st_poll传入超时参数>0或调用st_usleep和st_cond_timewait时,将thread加入SLEEPQ中。

 

posted on 2016-12-12 22:17  BestWill  阅读(9243)  评论(0编辑  收藏  举报

导航