coroutine协程详解
前兩天阿里巴巴開源了coobjc,沒幾天就已經(jīng)2千多star了,我也看了看源碼,主要關注的是協(xié)程的實現(xiàn),周末折騰了兩整天參照Go的前身libtask和風神的coroutine實現(xiàn)了一部分,也看了一些文章,稍微整理一下。
協(xié)程
Coroutines are computer-program components that generalize subroutines for non-preemptive multitasking, by allowing multiple entry points for suspending and resuming execution at certain locations. Coroutines are well-suited for implementing familiar program components such as cooperative tasks, exceptions, event loops, iterators, infinite lists and pipes.
Process -> Thread -> Coroutine
協(xié)程(Coroutine)編譯器級的,進程(Process)和線程(Thread)操作系統(tǒng)級的
進程(Process)和線程(Thread)是os通過調度算法,保存當前的上下文,然后從上次暫停的地方再次開始計算,重新開始的地方不可預期,每次CPU計算的指令數(shù)量和代碼跑過的CPU時間是相關的,跑到os分配的cpu時間到達后就會被os強制掛起,開發(fā)者無法精確的控制它們。
協(xié)程(Coroutine)是一種輕量級的用戶態(tài)線程,實現(xiàn)的是非搶占式的調度,即由當前協(xié)程切換到其他協(xié)程由當前協(xié)程來控制。目前的協(xié)程框架一般都是設計成 1:N 模式。所謂 1:N 就是一個線程作為一個容器里面放置多個協(xié)程。那么誰來適時的切換這些協(xié)程?答案是有協(xié)程自己主動讓出 CPU,也就是每個協(xié)程池里面有一個調度器,這個調度器是被動調度的。意思就是他不會主動調度。而且當一個協(xié)程發(fā)現(xiàn)自己執(zhí)行不下去了(比如異步等待網(wǎng)絡的數(shù)據(jù)回來,但是當前還沒有數(shù)據(jù)到),這個時候就可以由這個協(xié)程通知調度器,這個時候執(zhí)行到調度器的代碼,調度器根據(jù)事先設計好的調度算法找到當前最需要 CPU 的協(xié)程。切換這個協(xié)程的 CPU 上下文把 CPU 的運行權交個這個協(xié)程,直到這個協(xié)程出現(xiàn)執(zhí)行不下去需要等等的情況,或者它調用主動讓出 CPU 的 API 之類,觸發(fā)下一次調度。
優(yōu)缺點
優(yōu)點:
- 協(xié)程更加輕量,創(chuàng)建成本更小,降低了內存消耗
協(xié)程本身可以做在用戶態(tài),每個協(xié)程的體積比線程要小得多,因此一個進程可以容納數(shù)量相當可觀的協(xié)程
- 協(xié)作式的用戶態(tài)調度器,減少了 CPU 上下文切換的開銷,提高了 CPU 緩存命中率
協(xié)作式調度相比搶占式調度的優(yōu)勢在于上下文切換開銷更少、更容易把緩存跑熱。和多線程比,線程數(shù)量越多,協(xié)程的性能優(yōu)勢就越明顯。進程 / 線程的切換需要在內核完成,而協(xié)程不需要,協(xié)程通過用戶態(tài)棧實現(xiàn),更加輕量,速度更快。在重 I/O 的程序里有很大的優(yōu)勢。比如爬蟲里,開幾百個線程會明顯拖慢速度,但是開協(xié)程不會。
但協(xié)程也放棄了原生線程的優(yōu)先級概念,如果存在一個較長時間的計算任務,由于內核調度器總是優(yōu)先 IO 任務,使之盡快得到響應,就將影響到 IO 任務的響應延時。假設這個線程中有一個協(xié)程是 CPU 密集型的他沒有 IO 操作,也就是自己不會主動觸發(fā)調度器調度的過程,那么就會出現(xiàn)其他協(xié)程得不到執(zhí)行的情況,所以這種情況下需要程序員自己避免。
此外,單線程的協(xié)程方案并不能從根本上避免阻塞,比如文件操作、內存缺頁,這都屬于影響到延時的因素。
- 減少同步加鎖,整體上提高了性能
協(xié)程方案基于事件循環(huán)方案,減少了同步加鎖的頻率。但若存在競爭,并不能保證臨界區(qū),因此該上鎖的地方仍需要加上協(xié)程鎖。
- 可以按照同步思維寫異步代碼,即用同步的邏輯,寫由協(xié)程調度的回調
需要注意的是,協(xié)程的確可以減少 callback 的使用但是不能完全替換 callback。基于事件驅動的編程里面反而不能發(fā)揮協(xié)程的作用而用 callback 更適合。
缺點:
- 在協(xié)程執(zhí)行中不能有阻塞操作,否則整個線程被阻塞(協(xié)程是語言級別的,線程,進程屬于操作系統(tǒng)級別)
- 需要特別關注全局變量、對象引用的使用
- 協(xié)程可以處理 IO 密集型程序的效率問題,但是處理 CPU 密集型不是它的長處。
假設這個線程中有一個協(xié)程是 CPU 密集型的他沒有 IO 操作,也就是自己不會主動觸發(fā)調度器調度的過程,那么就會出現(xiàn)其他協(xié)程得不到執(zhí)行的情況,所以這種情況下需要程序員自己避免。
適用場景
- 高性能計算,犧牲公平性換取吞吐。協(xié)程最早來自高性能計算領域的成功案例,協(xié)作式調度相比搶占式調度而言,可以在犧牲公平性時換取吞吐
- IO Bound 的任務
在 IO 密集型的程序中由于 IO 操作遠遠小于 CPU 的操作,所以往往需要 CPU 去等 IO 操作。同步 IO 下系統(tǒng)需要切換線程,讓操作系統(tǒng)可以再 IO 過程中執(zhí)行其他的東西。這樣雖然代碼是符合人類的思維習慣但是由于大量的線程切換帶來了大量的性能的浪費。
所以人們發(fā)明了異步 IO。就是當數(shù)據(jù)到達的時候觸發(fā)我的回調。來減少線程切換帶來性能損失。但是這樣的壞處也是很大的,最大的問題就是破壞掉了人類這種線性的思維模式,你必須把一個邏輯上線性的過程切分成若干個片段,每個片段的起點和終點就是異步事件的完成和開始。固然經(jīng)過一些訓練你可以適應這種思維模式,但你還是要付出額外的心智負擔。與人類的思維模式相對應,大多數(shù)流行的編程語言都是命令式的,程序本身呈現(xiàn)出一個大致的線性結構。異步回調在破壞點思維連貫性的同時也破壞掉了程序的連貫性,讓你在閱讀程序的時候花費更多的精力。這些因素對于一個軟件項目來說都是額外的維護成本,所以大多數(shù)公司并不是很青睞 node.js 或者 RxJava 之類的異步回調框架,盡管這些框架能提升程序的并發(fā)能力。
但是協(xié)程可以很好解決這個問題。比如把一個 IO 操作 寫成一個協(xié)程。當觸發(fā) IO 操作的時候就自動讓出 CPU 給其他協(xié)程。要知道協(xié)程的切換很輕的。協(xié)程通過這種對異步 IO 的封裝既保留了性能也保證了代碼的容易編寫和可讀性。
- Generator 式的流式計算
消除 Callback Hell(回調地獄),使用同步模型降低開發(fā)成本的同時保留更靈活控制流的好處,比如同時發(fā)三個請求;這時節(jié)約地使用棧,可以充分地發(fā)揮 "輕量" 的優(yōu)勢。
ucontext
協(xié)程一般有兩類實現(xiàn),一種是 stackless,一種是 stackful。
structure
?
struct ucontext {/** Keep the order of the first two fields. Also,* keep them the first two fields in the structure.* This way we can have a union with struct* sigcontext and ucontext_t. This allows us to* support them both at the same time.* note: the union is not defined, though.*/sigset_t uc_sigmask; //這個上下文要阻塞的信號mcontext_tt uc_mcontextt; //保存的上下文的特定機器表示,包括調用線程的特定寄存器等struct __ucontext *uc_link; //指向當前的上下文結束時要恢復到的上下文stack_t uc_stack; //該上下文中使用的棧int __spare__[8]; };getcontext
?
int getcontext(ucontext_t *ucp)該函數(shù)初始化ucp所指向的結構體ucontext_t(用來保存前執(zhí)行狀態(tài)上下文),填充當前有效的上下文
setcontext
?
int setcontext(const ucontext_t *ucp)函數(shù)恢復用戶上下文為ucp所指向的上下文。成功調用不會返回。ucp所指向的上下文應該是getcontext()或者makecontext()產生的。
如果上下文是getcontext()產生的,切換到該上下文,程序的執(zhí)行在getcontext()后繼續(xù)執(zhí)行。
如果上下文被makecontext()產生的,切換到該上下文,程序的執(zhí)行切換到makecontext()調用所指定的第二個參數(shù)的函數(shù)上。當該函數(shù)返回時,我們繼續(xù)傳入makecontext()中的第一個參數(shù)的上下文中uc_link所指向的上下文。如果是NULL,程序結束。
成功時,getcontext()返回0,setcontext()不返回。錯誤時,都返回-1并且賦值合適的errno。
makecontext
?
void makecontext(ucontext_t *ucp, void (*func)(void), int argc, ...)函數(shù)修改ucp所指向的上下文,ucp是被getcontext()所初始化的上下文。當這個上下文采用swapcontext()或者setcontext()被恢復,程序的執(zhí)行會切換到func的調用,通過makecontext()調用的argc傳遞func的參數(shù)。
在makecontext()產生一個調用前,應用程序必須確保上下文的棧分配已經(jīng)被修改。應用程序應該確保argc的值跟傳入func的一樣(參數(shù)都是int值4字節(jié));否則會發(fā)生未定義行為。
當makecontext()修改過的上下文返回時,uc_link用來決定上下文是否要被恢復。應用程序需要在調用makecontext()前初始化uc_link。
swapcontext
?
int swapcontext(ucontext_t *restrict oucp, const ucontext_t *restrict ucp)函數(shù)保存當前的上下文到oucp所指向的數(shù)據(jù)結構,并且設置到ucp所指向的上下文。
成功完成,swapcontext()返回0。否則返回-1,并賦值合適的errno。
swapcontext()函數(shù)可能會因為下面的原因失敗:
ENOMEM ucp參數(shù)沒有足夠的棧空間去完成操作
ucontext協(xié)程的實際使用
將getcontext,makecontext,swapcontext封裝成一個類似于lua的協(xié)同式協(xié)程,需要代碼中主動yield釋放出CPU。
協(xié)程的棧采用malloc進行堆分配,分配后的空間在64位系統(tǒng)中和棧的使用一致,地址遞減使用,uc_stack.uc_size設置的大小好像并沒有多少實際作用,使用中一旦超過已分配的堆大小,會繼續(xù)向地址小的方向的堆去使用,這個時候就會造成堆內存的越界使用,更改之前在堆上分配的數(shù)據(jù),造成各種不可預測的行為,coredump后也找不到實際原因。
對使用協(xié)程函數(shù)的棧大小的預估,協(xié)程函數(shù)中調用其他所有的api的中的局部變量的開銷都會分配到申請給協(xié)程使用的內存上,會有一些不可預知的變量,比如調用第三方API,第三方API中有非常大的變量,實際使用過程中開始時可以采用mmap分配內存,對分配的內存設置GUARD_PAGE進行mprotect保護,對于內存溢出,準確判斷位置,適當調整需要分配的棧大小。
ucontext簇函數(shù)學習_實際使用.png
風神的coroutine
風神的coroutine是基于ucontext封裝的
schedule 調度器
?
struct schedule {char stack[STACK_SIZE]; // 原來schedule里面就已經(jīng)存有了stackucontext_t main; // ucontext_t你可以看做是記錄上下文信息的一個結構int nco; // 協(xié)程的數(shù)目int cap; // 容量int running; // 正在運行的coroutine的idstruct coroutine **co; // 這里是一個二維的指針 };coroutine
?
struct coroutine {coroutine_func func; // 運行的函數(shù)void *ud; // 參數(shù)ucontext_t ctx; // 用于記錄上下文信息的一個結構struct schedule * sch; // 指向scheduleptrdiff_t cap; // 堆棧的容量ptrdiff_t size; // 用于表示堆棧的大小int status;char *stack; // 指向棧地址么? };coroutine_new
?
int coroutine_new(struct schedule *S, coroutine_func func, void *ud)創(chuàng)建一個協(xié)程,該協(xié)程的會加入到schedule的協(xié)程序列中,func為其執(zhí)行的函數(shù),ud為func的執(zhí)行函數(shù)。返回創(chuàng)建的線程在schedule中的編號
coroutine_yield
?
void coroutine_yield(struct schedule * S)掛起調度器schedule中當前正在執(zhí)行的協(xié)程,切換到主函數(shù)。
coroutine_resume
?
void coroutine_resume(struct schedule * S, int id) {恢復運行調度器schedule中編號為id的協(xié)程
coroutine_close
?
void coroutine_close(struct schedule *S)關閉schedule中所有的協(xié)程
Coroutine及其實現(xiàn)
協(xié)程(Coroutine)-ES中關于Generator/async/await的學習思考
ucontext-人人都可以實現(xiàn)的簡單協(xié)程庫
協(xié)程 及 libco 介紹
我所理解的ucontext族函數(shù)
ucontext簇函數(shù)學習
進程與線程4_協(xié)程
構建C協(xié)程之ucontext篇
協(xié)程:posix::ucontext用戶級線程實現(xiàn)原理分析
ucontext-人人都可以實現(xiàn)的簡單協(xié)程庫
作者:小涼介
鏈接:https://www.jianshu.com/p/2782f8c49b2a
來源:簡書
著作權歸作者所有。商業(yè)轉載請聯(lián)系作者獲得授權,非商業(yè)轉載請注明出處。
總結
以上是生活随笔為你收集整理的coroutine协程详解的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Python生成器(send,close
- 下一篇: Python并发之协程gevent基础(