一、背景
為什麼需要網路協程?
1、協程/纖程並不是一個新概念
2、大併發、高效能對於服務端的高要求
3、移動裝置的快速增長加大了服務端大併發壓力
4、Go 語言的興起將協程帶到了一個新的高度
支援協程的程式語言:
1、Go 語言,非常容易支援大併發、高效能
2、Python 語言
3、Erlang 語言
4、Lua 語言
。。。。。。
為什麼要設計一套 C/C++ 網路協程庫?
1、學習一部門語言的成本要遠高於學習一個庫
2、C/C++ 程式設計師多年的經驗積累損耗巨大
3、C/C++ 綜合執行效率高
二、關於併發
– 雖已進入多核時代,但伺服器的 CPU 核心總是有限的
– 當程式/執行緒數越多作業系統的排程演算法就越低效
– TCP長連線及連線池的存在,造成服務端80%以上的連線是空閒的
為支援併發,我們需要採用:
1、多程式模式:支援併發能力非常有限,如 Postfix,Xinetd;
2、多執行緒模式:比多程式模式有提高,但依然有限,如 Mysql;
3、非阻塞模式:效能高,但程式設計複雜度極高,如 Nginx,Redis;
4、基於事件的多執行緒模式:併發度有較大提高,但程式設計提升依然有限,如 acl 中的 master_threads 服務模式;
三、設計目標
我們需要一種新的程式設計模式來滿足C/C++程式設計師:
1、支援大併發、高效能,較低的資源使用率
2、較低的程式設計複雜度:順序思維模式
3、適合多數應用場景,提供豐富且簡單易用的介面
4、與第三方網路庫無縫整合,無需修改第三方庫
四、一個簡單的協程示例
3、順序性程式設計方式
4、無需更改第三方庫
5、僅使用一個執行緒資源
五、協程的排程方式
或 自己通過組合語言來實現協程執行棧空間的切換
實現庫舉例:libtask,boost,libgo, libco,coroutine 等
2、訊號跳轉
六、協程切換方式
七、網路協程排程
1、IO事件協程監控所有的IO事件
2、網路協程執行時遇到IO阻塞,則被掛起,其IO控制程式碼由IO事件協程監控
3、IO事件發生時,其繫結的協程被再次喚醒
八、如何與第三方庫無縫整合
1、HOOK IO相關API
讀 API:read/readv/recv/recvfrom/recvmsg
寫 API:write/writev/send/sendto/sendmsg
其它 API:pipe/popen/pclose/open/close/fcntl
2、HOOK 網路相關API
socket/socketpair/bind/listen/accept/connect
poll/select/epoll_create/epoll_wait/epoll_ctl
gethostbyname/gethostbyname_r
通過 HOOK 系統底層 API,可以實現:
1、直接接管第三方庫(如:mysql/http/redis 等庫)的網路連線及通訊過程
2、直接接管第三方庫的域名解析過程
3、將第三方網路阻塞過程協程化,在協程庫底層轉化為非阻塞過程
將mysql庫協程化的例子參見:acl/lib_fiber/samples/mysql
九、為何要 HOOK 很多系統API
1、poll/select 為網路程式設計中常用系統 API
2、很多第三方網路庫用 poll/select 模擬IO超時
3、epoll 在 reactor 類應用(如:聊天)方面比較廣泛
4、gethostbyname 在域名解析方面應用廣泛
5、listen 需要將監聽描述字設為非阻塞模式
6、connect 需要將連線描述字設為非阻塞模式
7、bind/socket/socketpair/。。。為便於將出錯號與協程繫結
十、基於協程的 errno
因為每個執行緒中存在大量協程,當某個協程的IO過程出錯時,如果實現不同協程之間的 errno 是相互隔離的?
— 在 Linux 平臺下直接 HOOK __errno_location 系統函式
參見:/usr/include/bits/errno.h
extern int *__errno_location (void) __THROW __attribute__ ((__const__));
#define errno (*__errno_location ())
針對程式內全域性變數:errno,作業系統將該變數定義為一個函式指標地址,函式內部會通過執行緒區域性變數方式給每一個執行緒分配一個 error 物件
因此,通過 hook __errno_location 函式,在協程庫裡給每個協程一個協程區域性變數,實現了 errno 全域性變數的協程安全性
十一、記憶體安全檢測
配合 valgrind 做記憶體檢測:
– valgrind 與 xxxcontext 的不相容性
– 需下載 valgrind 開發包,呼叫 VALGRIND_STACK_REGISTER通知
valgrind 跳過檢測該記憶體區域
– 檢測時在 Makefile 裡開啟 –DUSE_VALGRIND 編譯選項,重新編譯 lib_fiber.a
十二、有效使用多核
每個執行緒一個獨立的協程排程器,通過建立多個執行緒使用多核
使用 acl master 伺服器框架,建立多程式使用多核,每個程式一個協程排程器
多執行緒示例參見:acl/lib_fiber/samples/redis_threads
多程式示例參見:acl/lib_fiber/samples/master_fiber
十三、協程同步原語
基於協程的協程鎖:
1、協程互斥鎖
2、協程讀寫鎖
十四、協程掛起與喚醒
— 協程掛起方式
1、主動讓出 CPU 控制權
當前執行的協程通過呼叫 acl_fiber_yield 主動讓出 CPU 控制權,協程排程器呼叫別的協程
2、指定休眠時間
當前執行的協程通過呼叫 acl_fiber_sleep 使當前協程休眠指定時間
3、IO阻塞被掛起
當前執行的協程等待IO完成時,需要將自身掛起
— 協程喚醒方式
1、主動 yield 的協程又重新獲得 CPU 控制權
2、處於休眠狀態的協程時間到達
3、因IO阻塞而被掛起的協程因IO準備好而被喚醒
示例參考:
1、yield 方式:acl/lib_fiber/samples/fiber
2、sleep 方式:acl/lib_fiber/samples/sleep
3、IO 方式:acl/lib_fiber/samples/select
十五、過載保護
十六、協程間通訊
協程間為什麼需要通訊?
1、業務邏輯的模組化
2、業務模組的分層設計
3、團隊開發的協作性
協程間“通訊”的本質:
– 協程間資料的傳遞通過協程上下文的切換,本質上是協程間的資料交換
協程間“通訊”的成本:
1、協程上下文切換
2、記憶體分配、釋放
3、資料拷貝
協程間“通訊”方式:
– 支援多對多資料互動
– 協程通訊管道支援多對多方式
– 協程間通訊通過切換協程上下文及資料交換完成
– 協程間通訊時的資料交換支援緩衝模式
– 協程間通訊時的資料交換採用隨機分配方式
十七、執行緒間通訊
協程模式下為何需要執行緒間通訊?
– 為使用多核,開啟多個執行緒,執行緒間需要交換資料
– 有些任務需要線上程池裡非同步完成,結果需要傳遞給主執行緒
協程模式下執行緒間的通訊方式:
– 無鎖訊息佇列 + IO 模式
十八、執行緒間通訊
1、生產者/消費者之間優先通過無鎖佇列進行資料傳遞
2、當生產者無資料時,消費者通過IO堵塞
3、當消費者堵塞在IO等待新訊息時,生產者若有新訊息則通過IO通知消費者
4、無鎖佇列利用率越高,則處理效能越高
十九、應用場景
(一)、問答式應用服務
基於 HTTP 協議的服務應用,諸如:網站
基於 SMTP/POP3/IMAP 協議的服務應用
(二)、生產者 – 消費者類應用服務
如訊息佇列類應用
(三)、reactor 和 proactor 兩種模式的結合
統一的事件引擎監控所有的網路連線,有一個連線就緒時建立協程獨立處理
此類應用如聊天服務、遊戲服務等無狀態的應用服務
(四)、大併發類應用服務
因為通過協程方式,將上層應用的堵塞式在底層轉為非阻塞模式,所以非常容易以較低資源支援大併發類應用
如內網的多數應用服務為提高效率都支援連線池模式,需要服務端支援非常大的併發
(五)、網路限流
在協程中可以直接 sleep,非常容易控制網路流量
二十、協程程式設計注意事項
(一)、協程執行堆疊空間的合理分配
每個協程都需要分配一定的記憶體空間用於上下文的切換,如果分配大了則會造成記憶體浪費,分配小了可能造成意外不可恢復的崩潰
一般情況下,每個協程分配32KB ~ 320KB
(二)、協程間需要協作,防止有的忙死,有的餓死
當協程長期佔用 CPU 時,應該主動 yield 讓出 CPU
(三)、協程內防止有堵塞式操作,以防堵塞當前執行緒中的所有協程
應通過對業務邏輯模組進行分類,確定不同的協程工作方式,使堵塞操作放線上程池中執行
二十一、資源下載
acl網路通訊與伺服器程式設計框架下載:https://github.com/zhengshuxin/acl
acl網路協程庫URL:https://github.com/zhengshuxin/acl/tree/master/lib_fiber