130行C語言實現個用戶態線程庫(2)


  仿制雲風的協程庫的接口設計,我花了一個下午加晚上的時間重構了之前寫的協程庫,提供的接口現在和雲風大大的協程接口一模一樣,都是仿制lua的非對稱協程。我們依舊沒有用ucontext.h組件(因為ucontext.h組件在osX下已經deprecated了,如果你加入sys/ucontext.h頭文件,發現某些小型C協程庫還能運行,純屬巧合,因為官方已經不維護這個組件了,以后很可能出錯),我們的協程庫可以運行在兼容X86平台的操作系統上,各種unix-like操作系統,windows操作系統都可以,不過得用gcc或者clang或者與之兼容的mingw編譯工具編譯出32位的程序運行,不能用vs或者vc++系列,因為我們用了gasm內聯匯編格式,當然你可以稍微改下幾行匯編就能移植到vs或者vc++版本。在多線程環境下運行協程時,不同線程不能共享協程組,也就是說任意兩個協程,若它們屬於不同的線程,那么它們得屬於不同的協程組。這是基本編程准則。比如想在POSIX多線程接口里頭用我們的協程庫時候,你可以這么用:

 1 #include <all needed>
 2  
 3 hello(schedular *s)
 4 { 5 coroutine_new(s, otherfunc, args); /* 在s協程組里頭創建一個協程, 這個s可能是S或S1等等 */ 6 ... do something 7 } 8 9 foo(...) 10 { 11 schedular *S1 =coroutine_open(); /* 分配一個協程組S1,這個只能屬於線程tid */ 12 int co = coroutine_new(S1, hello, S1); /* 創建一個協程 */ 13 ... do something 14 coroutine_resume(S1,co); /* 調用本地協程組里頭的co協程 */ 15 ... do something 16 coroutine_close(&S1); /* 釋放S1協程組 */ 17 } 18 19 main{ 20 schedular *S =coroutine_open(); /* 分配一個協程組S,這個只能屬於主線程 */ 21 pthread_create(tid, ..., foo, ...); /* 創建Posix線程 */ 22 int co = coroutine_new(S, hello, S); /* 創建一個協程 */ 23 ... do something 24 coroutine_resume(S,co); /* 調用本地協程組里頭的co協程 */ 25 ... do something 26 coroutine_close(&S); /* 釋放S協程組 */ 27 pthread_join(tid, ...); /* 等待並回收線程資源 */ 28 }

  如果要想不同線程間的協程通訊,得用操作系統各自的API,比如用共享內存方式來實現,我們沒有實現類似goroutine的channel。協程庫為共享棧模式,一個協程組可以容納最多一百萬個協程,每個協程共用128Kbytes棧空間,我用top命令監測了一下運行一百萬個協程的測試程序,此時該測試程序內存占用峰值為280M左右,可以推算每個協程內存占用峰值為280bytes左右。可以推斷,如果運行一千萬個協程,我們至少需要10個協程組,每個協程組280M,一共2800M = 2.7G左右 = 4G總理論空間 - 1G內核空間,所以我們的協程庫所能支持的最大協程數是1000萬,這是理論上限。用gprof測試程序性能得知,2999997次協程切換共用0.39秒,每次切換時間在130ns左右。最重要的是,我們的協程庫所有的源碼加起來大概只有400行左右,這還包括了微量的注釋和頭文件。如果能夠移植到X64版本,那么我們的協程庫可以輕松支持千萬數量的協程,可以在實際開發中使用,不再是單單只有教學意義的小玩意了。可惜的是在unix like的各種平台下,作為唯一的異步非阻塞IO組件,linux kernal實現的aio組件並不是那么成熟(可能要爛尾了),而且glibc中用線程+信號模擬的用戶態aio組件也是有很多bug存在。異步操作與協程的結合果然還是在語言層面(做編譯器前端)實現更好,而非在庫上實現。

項目GitHub鏈接:https://github.com/Yuandong-Chen/coroutine/tree/ezco.v.0.0.1 

 

后記:

  最近用setjmp.h組件,重構了項目,刪去了匯編代碼,把項目移植到了X64-macosx-clang版本(只兼容intel X64的處理器,osX操作系統以及Clang編譯器,不兼容其他任何變化,包括Linux,GCC,X86等等),可以支持上億個協程(思考下?為何?)。項目放在上面GitHub鏈接里頭的默認版本內。到此為止,我們的協程完成度近似libconcurrency庫。如何移植到X64-linux-gcc版本是一個問題,因為glibc里頭,我們無法在jmp_buf數組中通過偏移量取出rip邏輯寄存器的取值。所以為了達到可移植性,我們還是得用匯編寫一個自己的setjmp/longjmp函數,其實很多協程庫就是自己重寫了setjmp/longjmp以滿足兼容性。這樣也有個壞處,那就是我們得寫大量的匯編,對不同的C編譯器,不同的操作系統,不同的CPU都得寫一個對應的匯編版本,當然,可以通過內聯匯編的方式在一定程度上減輕一點點工作量。這種兼容性的體力活我就不去干了。

  這里給出為何能支持上億協程的答案:很簡單,我們以1000萬個協程占4G空間估計,那么64位機如果有128G內存的話,1000萬*128/4 = 3億左右的協程並發。當然,我想沒人會用3億協程並發,因為即便是8核16線程,負載為300000000/(16) = 1.8億協程/線程,除非你有超級計算機,那么才可以做到幾百協程/線程。

進一步需要做的:

1)徹底放棄共享棧,每個協程重新擁有自己獨立的棧空間。因為x64下的虛擬內存足夠大了,共享棧帶來的優勢太小,我們根本不需要幾億個協程並發,但是我們需要他們執行的足夠快。

2)放棄lua的resume-yield協程模型,改用erlang的spawn模型,從而能夠利用多核帶來的並行執行的優勢。

3)添加協程間的channel機制(一種消息傳遞機制)。

總的說來,我們相當於要把erlang的輕量級進程這部分做成C語言庫。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM