什麼是coroutine?接觸過的腦子裏確定會蹦出來不少詞:async-await,generator,channel,yield,高併發,甚至goroutine。其實,這些都是coroutine的外部表象,coroutine的本質是什麼?上古時期的計算機科學家們早就給出了概念,coroutine就是能夠中斷並恢復執行的subroutine,什麼是subroutine?就是你們熟知的函數。前端
你們先不要在腦子裏思考這個中斷和恢復執行具體要怎麼作,而是先創建一個概念模型,一個函數除了調用結束後返回caller,還能夠在調用中途返回caller,並能夠由caller在中斷的地方繼續執行,這就是coroutine了。因此coroutine也被叫作resumable function,可繼續函數,而且從定義上看,coroutine是subroutine的超集,也就是全部的function也都是coroutine,只不過他們沒有執行中斷和繼續這兩個操做。併發
因此看到這裏,先不要想太多,先明確了coroutine是能夠中斷並恢復的函數就行了,咱們而後來明確幾個概念,爲了後面提到的時候不會混亂。less
coroutine的暫停/中斷,也叫suspend,是coroutine暫停執行,並將控制流返回給調用者caller的過程。async
coroutine的恢復/繼續,也叫resume,是caller恢復coroutine執行的過程。函數
固然coroutine還包含了普通函數也有的調用(invoke)和返回(return)兩個操做。高併發
函數要中斷並從中斷處恢復,那麼從常識上考慮,就像線程切換cpu要保存寄存器狀態同樣,函數中斷前也要保存當前的狀態到一個持久的位置,而後中斷後這部分棧空間才能放心的交給caller去繼續用,否則恢復的時候現場都被破壞了就不對了。性能
保存當前狀態有兩種常見的實現,一種是說,既然函數中斷以後棧空間不能被別人改寫,寄存器的值要保存下來,那不如讓這個函數使用獨立的棧空間好了,這種實現就是有棧協程(stackful-coroutine)。函數調用前,保存調用者的全部寄存器,而後malloc一塊單獨的空間並把棧指針指過去,而後正常調用函數,被調用的函數天然就會在這塊獨立的棧空間上操做。中斷前,把全部寄存器都存到棧上,而後把棧指針指回調用者的棧空間,而且恢復調用者的寄存器,這就實現了有棧協程的暫停操做。優化
根據個人描述也能看出,有棧協程的實現須要底層操做,好比修改棧指針,保存寄存器等等,並且有棧協程須要大塊的棧空間分配,無論什麼樣的函數,每次調用都要malloc出來幾kb的空間,而且保存寄存器和恢復也須要必定的性能損失,現代處理器須要保存的寄存器每每有上百字節,仍是比較大的。線程
固然也不是說有棧協程很差,和後面要說的無棧協程相比,有棧協程不須要太多編譯器支持,仍是很棒棒的。著名的有棧協程實現包括Windows的Fiber,ucontext,fcontext,boost fiber等。指針
用Windows的Fiber舉例,調用起來是這樣的
void* main_func; void coro() { int i = 0; i++; SwitchToFiber(main_func); //suspend回main i++; } int main() { main_func = ConvertThreadToFiber(xxx); void* coro = CreateFiber(coro, xxx); SwitchToFiber(coro); //調用coro //coro suspend SwitchToFiber(coro); //讓他resume DeleteFiber(coro); }
除了有棧協程,另外一個常見的實現就是無棧協程(stackless coroutine),這是怎麼實現的呢?無棧協程須要編譯器的轉換工做,對於一個簡單的協程(僞代碼)
void fun() { int i = 0; i++; SUSPEND(); i++; return; }
編譯器會將他轉寫成一個對象(或相似物),以suspend的地方爲分界,將函數拆成幾部分,每一個部分爲一個單獨的函數,而後將局部變量都作成類成員,這樣每一個被拆出來的子過程均可以訪問這個成爲了成員變量的局部變量,最後,保存一個狀態變量,生成一個MoveNext函數(名字只是爲了表述方便),每次調用MoveNext,根據狀態變量的值,來執行前面被拆出來的不一樣的函數,好比上面的僞代碼,會被編譯器轉寫成如下的樣子(命名只是爲了表述方便)
struct fun_coroutine { int i; int __state = 0; void MoveNext() { switch(__state) { case 0: return __part0(); case 1: return __part1(); } } void __part0() { i = 0; i++; __state = 1; } void __part1() { i++; } };
調用者對fun的調用也會被轉寫成構造fun_coroutine,而後調用MoveNext成員函數,此時執行的是__part0,__part0的返回就是函數第一次suspend,調用者能夠選擇第二次調用MoveNext,這時被執行的就是__part1函數了。
由此看來,無棧協程的調用消耗的空間就是局部變量佔用的所有空間,相比有棧協程每次分配幾KB小不少,並且,無棧協程對象直接構造在調用者的棧上,意味着其中的成員(局部變量)也都在調用者的棧上,相比有棧協程把局部變量放在新開的空間上,CPU cache局部性更好,同時無棧協程的中斷和函數返回幾乎沒有區別,而有棧攜程的中斷須要保存上百字節的寄存器,而且,無棧攜程須要編譯器參與,那麼編譯器徹底能夠進行相似函數內聯,常量摺疊之類的操做,將協程的調用盡量優化到沒有。綜合性能會比有棧協程更好。
吹了這麼多,有棧協程好是好,但是須要編譯器支持,而對於C++這種巨複雜的語言,你加點什麼東西一要提防着不要影響其它feature和已有代碼,二要地方這些東西能不能和已有feature結合,不能衝突,三還要不能限定實現(好比C#的yield只能返回IEnumerator<T>),因此牙膏擠到了C++20甚至23,尚未正式確立加入語言。
我在說無棧協程suspend的性能和函數返回沒區別的地方,確定有人會反駁我說fun_coroutine會被new出來啊,異常之類的東西會增大overhead啊,實際上這些東西普通函數也有,你new了一個對象而後調用成員函數,和調用全局函數,區別大嗎,我感受是不大。此外有人會懷疑轉寫成對象+狀態會讓編譯器無法優化,實際上,llvm是直接支持coroutine的,若是你的語言編譯到llvm,你的前端能夠不把他轉換成對象給llvm看,而是直接用llvm的協程原語,剩下的丟給llvm去優化。