协程库st(state threads library)原理解析
協(xié)程庫(kù)state threads library(以下簡(jiǎn)稱st)是一個(gè)基于setjmp/longjmp實(shí)現(xiàn)的C語(yǔ)言版用戶線程庫(kù)或協(xié)程庫(kù)(user level thread)。
這里有一個(gè)基本的協(xié)程例子?http://www.csl.mtu.edu/cs4411.ck/www/NOTES/non-local-goto/coroutine.html, 可以了解setjmp和longjmp的基本用法。如還有不懂,請(qǐng)自行查閱其他資料。本文主要關(guān)注st基于setjmp和longjmp的實(shí)現(xiàn)原理及其程序結(jié)構(gòu)。
?
st基本介紹?http://state-threads.sourceforge.net/docs/st.html。
從中可以看出,IA(Internet Application)架構(gòu)演化歷史:
1.多進(jìn)程MP
以Apache為代表的web server。創(chuàng)建進(jìn)程服務(wù)新用戶,開銷過(guò)高。調(diào)度單位是進(jìn)程。
2.多線程MT
創(chuàng)建新線程服務(wù)新用戶,線程間上下文切換,鎖競(jìng)爭(zhēng)等等,增加了額外開銷。調(diào)度單位是線程。
3.事件驅(qū)動(dòng)狀態(tài)機(jī)EDSM
基于IO復(fù)用機(jī)制,實(shí)現(xiàn)大量并發(fā)請(qǐng)求的處理。但程序是一體的,并不是基于線程,新程序需要從頭開始。該模式下主要使用回調(diào)和狀態(tài)參數(shù)來(lái)進(jìn)行上下文切換,實(shí)際上是以一種非常艱難和痛苦的方式實(shí)現(xiàn)了類似線程和棧的思想。ESDM最大的問(wèn)題是其“將線性思路分解成大量的回調(diào)所固有的復(fù)雜性”,導(dǎo)致程序難以實(shí)現(xiàn),擴(kuò)展和維護(hù)。
傳統(tǒng)EDSM程序架構(gòu):
4.協(xié)程
調(diào)度單位減小到函數(shù),上下文切換不需要內(nèi)核參與,不存在系統(tǒng)調(diào)用。上下文切換開銷降到最低,系統(tǒng)調(diào)用降到最低,沒(méi)有鎖競(jìng)爭(zhēng),沒(méi)有信號(hào)處理。保留了程序?qū)φ?qǐng)求的線性處理邏輯,提高了程序的開發(fā)效率,可擴(kuò)展性和可維護(hù)性。
基于st的ESDM程序模型:
?
基于setjmp和longjmp實(shí)現(xiàn)協(xié)程庫(kù)基本步驟(下述線程指用戶線程):
1.需要用jmpbuf變量保存每一個(gè)線程的運(yùn)行時(shí)環(huán)境,稱為線程上下文context。
2.為每個(gè)線程分配(malloc/mmap)一個(gè)stack,用于該線程運(yùn)行時(shí)棧,該stack完全等效于普通系統(tǒng)線程的函數(shù)調(diào)用棧。該stack地址是在線程初始化時(shí)設(shè)置,所以不需要考慮setjmp時(shí)保存線程的棧上frames數(shù)據(jù)的問(wèn)題。
3.通過(guò)調(diào)用setjmp初始化線程運(yùn)行時(shí)上下文,將context數(shù)據(jù)存放到j(luò)mpbuf結(jié)構(gòu)中。然后修改其中的棧指針sp指向上一步分配的stack。根據(jù)當(dāng)前系統(tǒng)棧的增長(zhǎng)方向,將sp設(shè)置為stack的最低或最高地址。
4.線程退出時(shí),需要返回到一個(gè)安全的系統(tǒng)位置。即,需要有一個(gè)主線程main thread或idle thread來(lái)作為其他線程最終的退出跳轉(zhuǎn)地址。需要為主線程保存一個(gè)jmpbuf。
5.設(shè)置過(guò)main thread的jmpbuf后,需要跳轉(zhuǎn)到其他線程開始執(zhí)行業(yè)務(wù)線程。
6.實(shí)現(xiàn)一個(gè)context交換函數(shù),在多個(gè)線程之間進(jìn)行跳轉(zhuǎn):保存自己的jmpbuf,longjmp到另一個(gè)線程的jmpbuf。
?
st基于setjmp和longjmp的具體實(shí)現(xiàn):
線程初始化:
#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)之后,同時(shí)修改它的棧指針sp指向新分配的線程stack->sp地址。該sp指針用于該thread以后的棧frames數(shù)據(jù)存儲(chǔ)。
線程切換:
#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其中主要時(shí)MD_SETJMP保存當(dāng)前context,然后調(diào)用_st_vp_schedule()從_ST_RUNQ上取第一個(gè)可運(yùn)行的thread,并調(diào)用_ST_RESTORE_CONTEXT將該thread恢復(fù)運(yùn)行。
線程恢復(fù):
#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 DEBUGST_INIT_CLIST(&_ST_THREADQ); #endifif ((*_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); #endifreturn 0; }在st_init里面,創(chuàng)建primordial thread和idle thread。primordial thread作為起始線程當(dāng)有其他線程加入運(yùn)行隊(duì)列后從該線程切出,idle thread作為背景線程在沒(méi)有可運(yùn)行線程的時(shí)候執(zhí)行io調(diào)度函數(shù)分發(fā)事件。
st_init里面調(diào)用st_thread_create并不會(huì)開始執(zhí)行idle線程,創(chuàng)建其他線程也一樣,只有在直接或間接調(diào)用_st_vp_schedule之后才會(huì)開始執(zhí)行RUNQ上面的線程。_st_vp_schedule函數(shù)在_ST_SWITCH_CONTEXT中被調(diào)用。
?
st程序結(jié)構(gòu):
st底層基于event-driven select/poll/kqueue/epoll等IO復(fù)用機(jī)制。下面以epoll為例說(shuō)明st底層事件管理機(jī)制。
st中有IOQ,ZOMBIEQ,RUNQ,SLEEPQ等幾個(gè)隊(duì)列,用來(lái)存儲(chǔ)處于對(duì)應(yīng)狀態(tài)的threads。
- RUNQ中存儲(chǔ)的是可以被調(diào)度運(yùn)行的threads,每次調(diào)用_st_vp_schedule即從該隊(duì)列取出一個(gè)thread去運(yùn)行。
- IOQ存儲(chǔ)處于IO等待狀態(tài)的threads,當(dāng)上層調(diào)用st_poll時(shí),將該thread放入IOQ中;當(dāng)?shù)讓觘poll有IO事件到達(dá)時(shí),將該thread從IOQ中移除,并放入RUNQ中。
- 當(dāng)thread退出時(shí),放入ZOMBIEQ中。
- 當(dāng)st_poll傳入超時(shí)參數(shù)>0或調(diào)用st_usleep和st_cond_timewait時(shí),將thread加入SLEEPQ中。
總結(jié)
以上是生活随笔為你收集整理的协程库st(state threads library)原理解析的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: SRS-DOLPHIN
- 下一篇: st(state-threads) co