什么是協(xié)程(coroutine)
Wiki定義:
Coroutines are computer program components that generalize subroutines for non-preemptive multitasking, by allowing execution to be suspended and resumed.
協(xié)同程序是一種計算機程序組件,它通過允許暫停和恢復(fù)執(zhí)行,將子程序泛化以實現(xiàn)非搶占式的多任務(wù)處理。
我的理解是協(xié)程是一種輕量級線程,也是一種調(diào)度的單位,只是這種調(diào)度是用戶(或者當(dāng)前線程)主動進行的,操作系統(tǒng)感知不到。
每個協(xié)程擁有自己的執(zhí)行上下文,當(dāng)需要讓出執(zhí)行的時候,協(xié)程就把當(dāng)前的上下文(寄存器、棧等等)保存起來,然后把其他協(xié)程的上下文恢復(fù),以達到切換的目的。這種切換是完全在用戶態(tài)進行,所以切換的性能比較髙。線程的切換需要進內(nèi)核,所以性能慢一些,通常比協(xié)程慢一個數(shù)量級。
協(xié)程 vs 線程
- 線程是搶占式的,協(xié)程是非搶占式的。
- 線程數(shù)量通常不可以太多,而協(xié)程的數(shù)量可以非常多。如果線程的數(shù)量太多,那么大量線程的切換會影響性能。
- 協(xié)程的調(diào)度是應(yīng)用代碼自己實現(xiàn)的,而線程的調(diào)度是操作系統(tǒng)實現(xiàn)的。
Stackfull協(xié)程 vs stackless協(xié)程
Stackless類型不保存調(diào)用棧以及寄存器等信息,不屬于真正的重入,因此一些局部變量都是無法使用的。
Stackfull是真正的基于棧的重入,可以從某個嵌套的調(diào)用點上恢復(fù)執(zhí)行。Stackfull協(xié)程在切換的時候,需要把當(dāng)前的棧保存起來,以便在恢復(fù)的時候再次恢復(fù)執(zhí)行,而stackless的則不需要。
對稱(symmetric) vs 非對稱(asymmetric)
協(xié)程依控制轉(zhuǎn)移機制分為對稱與非對稱,對稱協(xié)程中所有的協(xié)程都只有一種控制轉(zhuǎn)移語義,即將控制轉(zhuǎn)移至其他協(xié)程,此中的協(xié)程在這種機制下都是對稱的。而非對稱協(xié)程中調(diào)用者協(xié)程與被調(diào)用協(xié)程處于不對稱的地位,和函數(shù)調(diào)用一樣,被調(diào)用協(xié)程掛起之后控制總是會轉(zhuǎn)移到調(diào)用者協(xié)程。
協(xié)程優(yōu)點
個人理解,協(xié)程的唯一優(yōu)點:
目前看來,協(xié)程最主要的優(yōu)點,就是可以使用同步的方式寫異步執(zhí)行的代碼,使得代碼邏輯更加簡潔和清晰。
但是協(xié)程實際上還是無法完全避免寫異步回調(diào),因為我們需要在異步API的基礎(chǔ)上,結(jié)合協(xié)程庫,開發(fā)出“同步”的API。
很多文章中提出,協(xié)程的性能更高,個人覺得這個結(jié)論有待商榷。協(xié)程由于常常是自己實現(xiàn)的,所以自己實現(xiàn)的代碼質(zhì)量對性能有很大影響。另外拋開業(yè)務(wù)模式去談性能,也不是很合理。再者,很多人說線程的切換怎么樣怎么樣的,協(xié)程可以有成千上萬個,線程成千上萬個就怎么樣的,我覺得也很不客觀。因為實際上,我們實現(xiàn)的時候,線程數(shù)通常都是固定的,或者是跟CPU核數(shù)有一定關(guān)系的,比如CPU核數(shù)是8,那么線程數(shù)可以也是8,甚至可以做到線程和CPU綁定,那么這個線程切換本身就是一個小概率事件,甚至理論上如果沒有別的線程在跑,我們的線程可以做到不切換,所以根本就沒有切換性能這一說。
協(xié)程的應(yīng)用場景
我覺得協(xié)程的應(yīng)用場景可能對于一些非常底層的,例如內(nèi)核驅(qū)動、存儲、數(shù)據(jù)庫這樣的應(yīng)用,未必是合適的。而對于一些比較上層的應(yīng)用,例如即時通信軟件,可能是比較合適的。
協(xié)程有什么風(fēng)險?
- 無論是自己實現(xiàn),還是采用第三方的庫,都存在bug的風(fēng)險。
- 多線程場景下的并發(fā)安全問題。
- 新技術(shù)的接受問題。目前團隊中大家都熟悉了異步代碼的編寫方式,對于性能要求比較高的場景,API一般都是給出異步的,結(jié)合trace機制后,代碼也不難調(diào)試,性能也足夠。切換成協(xié)程后,可能反而有一定的接受難度。
- 我相信協(xié)程在使用過程中,還會出現(xiàn)一些其它讓我們始料未及的問題。
- 對于thread local storage(TLS)的場景,需要重新考慮。目前我們基于TLS做了很多優(yōu)化,這些優(yōu)化在協(xié)程場景下,是否還正確?是否還可用?可能需要重新評估。
- Debug上可能會有難度。例如如果core dump發(fā)生了,進程里有幾千個協(xié)程,每個協(xié)程執(zhí)行的情況,可能很難看出來。
- 其它未考慮到的風(fēng)險
協(xié)程對于我們團隊來說有沒有必要?
鑒于以上對協(xié)程的優(yōu)缺點的分析,我個人認(rèn)為,協(xié)程對我們團隊來說,的確是一個可選項,可以考慮在比較上層(性能要求不是非常嚴(yán)苛)的場景下使用,會在一定程度上加快開發(fā)的速度。但協(xié)程對于我們來說,遠遠達不到必選項的條件。
目前我們的大部分基礎(chǔ)庫都是采用異步的方式實現(xiàn)的,我們的大部分業(yè)務(wù)代碼,也是異步的,也就是用到了各種回調(diào)。其實難度也并沒有想象那么大,類似的代碼寫習(xí)慣了,其實也挺簡單的。后端的業(yè)務(wù)流程,個人覺得,寫異步代碼的困難程度,并不是瓶頸所在。
Implement in C
宏
Simon Tatham通過C語言的宏,實現(xiàn)了一種協(xié)程,參考這里。通過這個例子可以學(xué)習(xí)到協(xié)程的一些定義。不過上面的實現(xiàn)很難直接用在生產(chǎn)環(huán)境,一方面它會對已有的源碼有一些沖突,需要對現(xiàn)有的代碼進行重構(gòu)。另一方面它在宏里有一些malloc的動作,生產(chǎn)環(huán)境中可能不一定允許有這樣的操作。
基于ucontext的實現(xiàn)
風(fēng)云主要直接使用了現(xiàn)成的ucontext族的接口,這個接口從調(diào)研來看,主要的缺點在于性能慢,因為用到了系統(tǒng)調(diào)用。另外這個版本用到的是共享棧的做法。
代碼量非常少,比較直觀,不過基本上沒有做錯誤處理,要用于生產(chǎn)環(huán)境的話,需要自己再改一下。代碼托管在:https://github.com/cloudwu/coroutine/。
C的另外一個實現(xiàn):
http://xmailserver.org/libpcl.html