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