協程庫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中。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM