溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

為什么C/C++程序員都要閱讀Redis源碼之:Redis學習事件驅動設計

發布時間:2020-08-01 16:59:34 來源:網絡 閱讀:626 作者:CPPLinux開發 欄目:編程語言
  1. 為什么我說C/C++程序員都要閱讀Redis源碼

主要原因就是『簡潔』。如果你用源碼編譯過Redis,你會發現十分輕快,一步到位。其他語言的開發者可能不會了解這種痛,作為C/C++程序員,如果你源碼編譯安裝過Nginx/Grpc/Thrift/Boost等開源產品,你會發現有很多依賴,而依賴本身又有依賴,十分痛苦。通常半天一天就耗進去了。由衷地羨慕 npm/maven/pip/composer/...這些包管理器。而Redis則給人驚喜,一行make了此殘生。

除了安裝過程簡潔,代碼也十分簡潔。使用純C語言編寫,每個模塊功能都劃分的很清晰。

廢話不多說,本文要介紹的是Redis里的事件處理功能,與Memcache引入libevent這一臃腫的事件庫不同,Redis自己實現了一個小型輕量的事件驅動庫——AE。閱讀它的源碼是一次非常好的學習和體驗。

為什么C/C++程序員都要閱讀Redis源碼之:Redis學習事件驅動設計

  1. 跨平臺兼容:重劍無鋒,大巧不工

文件名說明ae.h/ae.c主要文件,根據OS平臺的不同依賴以下不同文件:

ae_epoll.c:Linux平臺
ae_kqueue.c:BSD平臺
ae_evport.c:Solaris平臺
ae_select.c:其他Unix平臺
雖然源碼文件看起來不少,但是實際上ae_epoll.c、 ae_kqueue.c、 ae_evport.c、 ae_select.c 這4個文件的功能是完全一樣的,提供一致的API接口,給ae.c文件調用。這是由于實現高性能的事件驅動的API(稱之為polling API)不存在ANSI或POSIX的標準,不同的OS內核有著自己的實現。比如Linux內核的epoll,BSD內核中的kqueue。

ae.c中有:

#ifdef HAVE_EVPORT
#include "ae_evport.c"
#else
#ifdef HAVE_EPOLL
#include "ae_epoll.c"
#else
#ifdef HAVE_KQUEUE
#include "ae_kqueue.c"
#else
#include "ae_select.c"
#endif
#endif
#endif
這些HAVE的宏,都是由在config.h中定義的。依據不同的操作系統,引入這4個文件中的某一個。從功能上來說,這樣設計的目的與GOF設計模式中“適配器模式”(修改成一致接口)或“外觀模式”(抽象復雜接口為簡單接口)的思想類似,但實際上我個人感覺更類似于《POSA》(卷二)中提到的“包裝門面模式”(Wrapper Facade)。Anyway,這個編程思想值得學習。

為什么C/C++程序員都要閱讀Redis源碼之:Redis學習事件驅動設計

  1. 用C++去設計,用C編碼:aeEventLoop

十幾年前,以Linux之父炮轟C++為開端,社區內展開了一場C與C++孰是孰非的論戰。而在國內,以原CSDN總編劉江援引此文為始,把戰火燒到了國內。孟巖、云風、pongba幾位大佬都身陷其中。后來以孟巖的一句『用C設計,用C++編碼』在國內為這場論戰定下基調。

反觀Redis,他是純C編碼,但是融入了面向對象的思想。和上述觀點截然相反,可謂是『用C++去設計,用C編碼』。當然本文目的并非挑起語言之爭,各種語言自有其利弊,開源項目的語言選擇也主要是由于項目作者的個人經歷和主觀意愿。

定義在ae.h中的結構體 aeEventLoop 是AE庫中最核心的數據結構,并且它采用了面向對象的設計思想:ae.h 中聲明了多個函數,其第一個參數都是一個aeEventLoop指針,用于操縱aeEventLoop結構體。從這個角度來說,可以將該結構體理解為面向對象語言中的類,而操縱它的函數則可以視為其成員函數。(其實C++的class編譯之后大概也是類似的模式)

aeCreateEventLoop:初始化一個事件循環結構體(eventLoop)
aeGetSetSize:返回當前setsize的值aeResizeSetSize改變setsize的值(空間重新分配)
aeDeleteEventLoop刪除事件循環eventLoop(釋放內存空間)
aeStop:停止事件循環,即stop值設為1
aeProcessEvents:核心部分:事件處理邏輯
aeMain:啟動事件循環,事件循環的入口
aeSetBeforeSleepProc:注冊回調函數,即每次主循環在休眠之前被調用
函數 aeCreateEventLoop 和 aeDeleteEventLoop 可以視為“類”aeEventLoop的構造和析構函數,其他為成員函數。

調用流程

在客戶程序調用AE庫的時候,一般是依次調用:

aeCreateEventLoop
aeSetBeforeSleepProc
aeMain
aeDeleteEventLoop
2.1 AE的兩種事件

事件處理,是有別于多線程/多進程的并發模型。我也都知道Redis是單線程的。它的性能主要依靠異步事件處理功能來實現。雖然事件處理通常和網絡編程混作一談,但其實事件處理本身不一定是為網絡編程服務的,它主要是服務于IO,網絡通信是IO,文件讀寫同樣是。當然Unix中萬物皆文件了,socket也是一種fd。

AE支持兩種事件:

文件事件(IO)時間事件(毫秒級)
這兩種事件都作為aeEventLoop的結構體成員存在。

aeEventLoop各成員說明:

typedef struct aeEventLoop {
int maxfd; / 當前注冊的最大fd /
int setsize; / 監視的fd的最大數量 /
long long timeEventNextId; / 下一個時間事件的ID /
time_t lastTime; / 上次時間事件處理時間 /
aeFileEvent events; / 已注冊文件事件數組 /
aeFiredEvent
fired; / 就緒的文件事件數組 /
aeTimeEvent timeEventHead; / 時間事件鏈表的頭 /
int stop; /
是否停止(0:否;1:是)/
void
apidata; / 各平臺polling API所需的特定數據 /
aeBeforeSleepProc beforesleep; / 事件循環休眠開始的處理函數 /
aeBeforeSleepProc
aftersleep; / 事件循環休眠結束的處理函數 /
} aeEventLoop;
文件事件,主要依靠兩個數組。一個是注冊的文件事件數組,一個是已就緒的文件事件數組。

typedef struct aeFileEvent {
int mask; / one of AE_(READABLE|WRITABLE|BARRIER) /
aeFileProc rfileProc;
aeFileProc
wfileProc;
void *clientData;
} aeFileEvent;
typedef struct aeFiredEvent {
int fd;
int mask;
} aeFiredEvent;
單詞Fired在這里表示的是就緒的意思,不知道是antirez英語不好,還是我英語不好,反正我是不知道fire有這層含義。

更新:經@Serena Yu提醒fire event是英語表示事件發出。果然還是我英語不好……

每個文件事件,其讀寫設置了不同的處理函數。另外mask表示事件的觸發類型。當每次polling API返回就緒之后(比如epoll_wait返回),就緒會被設置到aeFireEvent,然后反查aeFileEvent獲得處理函數并處理。你會發現aeFileEvent結構體里并沒有記錄fd。其實這是使用了HASH策略,aeEventLoop的成員 aeFileEvent數組的下標即是fd,便于快速查找。

時間事件,本質就是定時器任務,其數據結構采用一個雙向鏈表。鏈表每個結點為aeTimeEvent結構體,主要包含事件的ID(遞增)、就緒的時間,處理函數、清理函數、客戶數據。

typedef struct aeTimeEvent {
long long id; / time event identifier. /
long when_sec; / seconds /
long when_ms; / milliseconds /
aeTimeProc timeProc;
aeEventFinalizerProc
finalizerProc;
void clientData;
struct aeTimeEvent
prev;
struct aeTimeEvent *next;
} aeTimeEvent;
每個事件循環中,每個時間事件的ID唯一且遞增,主要依賴aeEventLoop里的timeEventNextId來維護這個ID的遞增關系。創建新的時間事件時(aeCreateTimeEvent)會賦值,由于只考慮了單線程,所以沒有加鎖邏輯,大家也不要貿然把AE用在多線程環境中。另外創建時間過程就是簡單的追加到時間事件鏈表的尾部,并沒有針對就緒時間做排序。

when_sec和 when_ms 記錄了時間事件的就緒時間(秒+毫秒),即當當前時間大于等于這個時間的時候,該時間事件應被處理。

時間事件的處理過程(processTimeEvents)主要就是:繼續遍歷鏈表,如果發現節點狀態為AE_DELETED_EVENT_ID則刪除該節點。如果判斷當前時間已經超過節點的就緒時間就開始處理。處理函數的返回值可以指定,后續不再處理該事件(NOMORE),則該節點會被置為AE_DELETED_EVENT_ID。如果下次還需要處理,則更新該節點的時間為下次就緒時間。

為什么C/C++程序員都要閱讀Redis源碼之:Redis學習事件驅動設計
2.2 事件循環的處理邏輯

再用一張圖,回顧一下EventLoop中的兩種事件,基本可以做如下理解。一個鏈表,一個數組。文件事件中的數組不是線性填滿的,因為是采用的HASH策略,將fd作為數組下標了。

為什么C/C++程序員都要閱讀Redis源碼之:Redis學習事件驅動設計
aeProcessEvents是aeEventLoop在循環過程中的的實際處理邏輯。

aeProcessEvents 是aeEventLoop在循環過程中的的實際處理邏輯。函數原型如下:

int aeProcessEvents(aeEventLoop *eventLoop, int flags);
flags標記,表示本次需要處理的事件類型標記和是否阻塞標記。

AE_TIME_EVENTS:時間事件標記
AE_FILE_EVENTS:文件事件標記
AE_DONT_WAIT:立即返回不阻塞等待的標記
aeProcessEvents代碼我就不貼了。它巧妙的地方是一次柔和了文件和時間事件的兩種處理過程。

在函數之初,會線性查找時間事件的鏈表,找到最近時間內會就緒時間事件,然后用它的就緒時間減去當前時間的時間差作為polling API的休眠時間(epoll_wait的timeout參數)。然后休眠等待polling api返回。在返回之后先執行aftersleep的的處理邏輯,然后執行這段休眠時間內就緒的文件事件,最后再處理就緒的時間事件。返回值是處理過的事件總數。

也就說AE會盡量在一次處理過程中,將時間事件和文件事件一次性處理。你也許會問如果沒有時間事件怎么辦。當然沒關系,在aeProcessEvents開始部分就根據標記位進行了判斷。上面的邏輯是在文件事件和時間事件都存在的情況下,如果僅存在文件事件,則看是否設置了不阻塞的標記(AE_DONT_WAIT),若有,則polling 的超時時間設置為0。如無,即可以阻塞,則設置為-1,則polling API會阻塞直到有文件事件發生。

縱觀AE是一種非常簡易但也十分典型的Reactor網絡模型。

ae_epoll(Linux上polling API:epoll的封裝)

前文說道Redis適配各種Unix-like的操作系統。它將系統強相關的事件API部分單獨抽出來,包裝出了相同的接口給AE的對外API調用。在Linux系統上的API實現為:ae_epoll.c,建議在閱讀這個文件源碼之前先好好回顧一下epoll的API,這樣更助于快速理解。相信工作后大家寫業務邏輯,應該很少接觸epoll了??梢蚤喿x這個wik,快速回顧epoll的api:LinuxAPI:epoll

ae_epoll.c 完全被 ae.c調用。各函數調用關系如下(aeApi開頭的都是ae_epoll.c中的函數):

ae.c
aeCreateEventLoop
aeApiCreate
aeResizeSetSize
aeApiResize
aeDeleteEventLoop
aeApiFree
aeCreateFileEvent
aeApiAddEvent
aeDeleteFileEvent
aeApiDelEvent
aeProcessEvent
aeApiPoll
aeGetApiName
aeApiName
除了aeApiName()以外,其他函數第一個參數也都是aeEventLoop * 。用面向對象的思想來看這也是aeEventLoop的成員函數。試想若是C++,則可能會被處理成父子兩個類,而aeApi系列的函數是純虛的。

aeApi的函數也是可以做到顧名即可思義。比如:

aeApiCreate在堆上進行內存的分配,封裝epoll_create創建epfd,并寫入aeEventLoop。
aeApiAddEvent、aeApiDelEvent是封裝的epoll_ctl來對aeEventLoop的監控的epoll事件進行添加和刪除。
aeApiPoll是封裝的epoll_wait開啟事件循環,并且每次取出就緒的fd存入aeEventLoop的fired數組中,并置位相應的mask(讀or寫)
aeApiResize、aeApiFree分別進行的是內存的重分配、資源的清理(關閉epfd,free內存)和epoll本身關聯不大。
不止AE中,整個Redis在使用堆內存的時候,都是使用它自己實現的zmalloc,而非libc的malloc。

Jim:吃水不忘挖井人,AE的靈感之源

閱讀完AE代碼,可能只需要一下午的時間,你會驚嘆于作者的設計功力。其實里面也沒有太多花哨的東西,但就是如此簡潔清晰的給你呈現了一個完成度如此之高的事件驅動處理庫。但我想即使大家都熟悉epoll、熟悉kqueue、熟悉數據結構也不一定能設計出來AE,所以把程序員比作代碼的設計師、建筑師是絲毫不為過的。

“吃水不忘挖井人”,AE的設計靈感也是受另外一個開源項目影響,它就是 Jim。Redis的ae.c的開篇注釋中就已注明:

/* A simple event-driven programming library. Originally I wrote this code

  • for the Jim's event-loop (Jim is a Tcl interpreter) but later translated
  • it in form of a library for easy reuse.
    Jim的源碼在Github上有它的鏡像,其中事件循環的代碼在此:https://github.com/msteveb/jimtcl/blob/master/jim-eventloop.c

簡單閱讀一下,你就會發現AE確實整體的處理邏輯是從Jim吸收的。但AE也不乏創新,比如抽象除polling API這層,達到了多平臺的兼容和解耦,而Jim強耦合了select。

Anyway,就像經典的『站在巨人肩膀』理論,雖然Jim不是巨人,但它啟發了AE,即使Jim最終被世人遺忘,而它的血肉也化作了土壤,滋養后來人,這就是開源運動的意義所在,也是魅力所在。

向AI問一下細節

免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

AI

亚洲午夜精品一区二区_中文无码日韩欧免_久久香蕉精品视频_欧美主播一区二区三区美女