大家好,我是極客重生,假期這么快就結(jié)束了,不管做什么,都要認(rèn)真做好,該玩的時候就好好玩,放松休息,該學(xué)習(xí)的時候就好好學(xué)習(xí),刻苦訓(xùn)練,該工作的時候就認(rèn)真工作,努力掙錢,今天我們看一看業(yè)界一些著名的編程模型。
背景
模型是對事物共性的抽象,編程模型就是對編程的共性的抽象。當(dāng)面對一個新問題時,通常的想法是通過分析,不斷的轉(zhuǎn)化和轉(zhuǎn)換,得到本質(zhì)相同的熟悉的、或抽象的、簡單的一個問題,這就是化歸思想。把初始的問題或?qū)ο蠓Q為原型,把化歸后的相對定型的模擬化或理想化的對象稱為模型。
編程模型,簡單地可以理解它就是模板,遇到相似問題就可以方便依模板解決,這樣就簡化了編程問題。不同的編程環(huán)境和不同的應(yīng)用對象有不同的編程模型。
事件驅(qū)動
來源于《Software Architecture Patterns》事件驅(qū)動架構(gòu)(Event-Driven Architecture)是一種用于設(shè)計應(yīng)用的軟件架構(gòu)和模型,程序的執(zhí)行流由外部事件來決定,它的特點是包含一個事件循環(huán),當(dāng)外部事件發(fā)生時使用回調(diào)機制來觸發(fā)相應(yīng)的處理。主要包括 4 個基本組件:
- 事件隊列(event queue):接收事件的入口,存儲待處理事件
- 分發(fā)器(event mediator):將不同的事件分發(fā)到不同的業(yè)務(wù)邏輯單元
- 事件通道(event channel):分發(fā)器與處理器之間的聯(lián)系渠道
- 事件處理器(event processor):實現(xiàn)業(yè)務(wù)邏輯,處理完成后會發(fā)出事件,觸發(fā)下一步操作
為什么采用事件驅(qū)動模型?
- 首先是一種對象間的一對多的關(guān)系;最簡單的如交通信號燈,信號燈是目標(biāo)(一方),行人注視著信號燈(多方);
- 當(dāng)目標(biāo)發(fā)送改變(發(fā)布),觀察者(訂閱者)就可以接收到改變;
- 觀察者如何處理(如行人如何走,是快走/慢走/不走,目標(biāo)不會管的),目標(biāo)無需干涉;所以就松散耦合了它們之間的關(guān)系。
- 松耦合——服務(wù)不需要(也不應(yīng)該)知道或依賴于其他服務(wù)。在使用事件時,服務(wù)獨立運行,不了解其他服務(wù),包括其實現(xiàn)細(xì)節(jié)和傳輸協(xié)議。事件模型下的服務(wù)可以獨立地、更容易地更新、測試和部署。
- 易擴展——通過高度獨立和解耦的事件處理器自然地實現(xiàn)了可擴展性。每個事件處理器都可以單獨擴展,從而實現(xiàn)細(xì)粒度的可擴展性。
- 恢復(fù)支持——帶有隊列的事件驅(qū)動架構(gòu)可以通過“重播”過去的事件來恢復(fù)丟失的工作。當(dāng)用戶需要恢復(fù)時,這對于防止數(shù)據(jù)丟失非常有用。
事件驅(qū)動架構(gòu)可以最大程度減少耦合度,因此是現(xiàn)代化分布式應(yīng)用架構(gòu)的理想之選。
深入理解事件驅(qū)動1.異步處理和主動輪訓(xùn),要理解事件驅(qū)動和程序,就需要與非事件驅(qū)動的程序進(jìn)行比較。實際上,現(xiàn)代的程序大多是事件驅(qū)動的,比如多線程的程序,肯定是事件驅(qū)動的。早期則存在許多非事件驅(qū)動的程序,這樣的程序,在需要等待某個條件觸發(fā)時,會不斷地檢查這個條件,直到條件滿足,這是很浪費cpu時間的。而事件驅(qū)動的程序,則有機會釋放cpu從而進(jìn)入睡眠態(tài)(注意是有機會,當(dāng)然程序也可自行決定不釋放cpu),當(dāng)事件觸發(fā)時被操作系統(tǒng)喚醒,這樣就能更加有效地使用cpu。2.IO模型,事件驅(qū)動框架一般是采用Reactor模式或者Proactor模式的IO模型。Reactor模式其中非常重要的一環(huán)就是調(diào)用函數(shù)來完成數(shù)據(jù)拷貝,這部分是應(yīng)用程序自己完成的,內(nèi)核只負(fù)責(zé)通知監(jiān)控的事件到來了,所以本質(zhì)上Reactor模式屬于非阻塞同步IO。來自:深入理解Linux高性能網(wǎng)絡(luò)架構(gòu)的那些事
Proactor模式,借助于系統(tǒng)本身的異步IO特性,由操作系統(tǒng)進(jìn)行數(shù)據(jù)拷貝,在完成之后來通知應(yīng)用程序來取就可以,效率更高一些,但是底層需要借助于內(nèi)核的異步IO機制來實現(xiàn),可能借助于DMA和Zero-Copy技術(shù)來實現(xiàn),理論上性能更高。
當(dāng)前Windows系統(tǒng)通過IOCP實現(xiàn)了真正的異步I/O,而在Linux 系統(tǒng)的異步I/O還不完善,比如Linux中的boost.asio模塊就是異步IO的支持,但是目前Linux系統(tǒng)還是以基于Reactor模式的非阻塞同步IO為主。
3.事件隊列,事件驅(qū)動的程序必定會直接或者間接擁有一個事件隊列,用于存儲未能及時處理的事件,這個事件隊列,可以采用消息隊列。4.事件串聯(lián),事件驅(qū)動的程序的行為,完全受外部輸入的事件控制,所以事件驅(qū)動框架中,存在大量處理程序邏輯,可以通過事件把各個處理流程關(guān)聯(lián)起來。5.順序性和原子化,事件驅(qū)動的程序可以按照一定的順序處理隊列中的事件,而這個順序則是由事件的觸發(fā)順序決定的,這一特性往往被用于保證某些過程的順序性和原子化。
事件驅(qū)動的缺點
- 事件驅(qū)動架構(gòu),就是通過引入中間層 來實現(xiàn)事件發(fā)布-訂閱機制進(jìn)行組件解耦,看似能帶來不少誘人的優(yōu)點,也必然會增加系統(tǒng)的復(fù)雜度,間接增加開發(fā)難度和維護難度。
- 事件驅(qū)動架構(gòu)改變了編程思維,將完整的功能過程,拆解為了不同的異步事件處理,也喪失了連貫的流程處理能力。如果事件數(shù)量眾多,就容易在“事件叢林”中迷了路,比如中斷風(fēng)暴,驚群效應(yīng)等。
常用的事件驅(qū)動框架
select
poll
epoll
- libev
- 中斷系統(tǒng)
消息驅(qū)動
消息驅(qū)動和事件驅(qū)動很類似,都是先有一個事件,然后產(chǎn)生一個相應(yīng)的消息,再把消息放入消息隊列,由需要的項目獲取。他們只是一些細(xì)微區(qū)別,一般都采用相同框架,細(xì)微的區(qū)別:
消息驅(qū)動:生產(chǎn)者A發(fā)送一個消息到消息隊列,消費者B收到該消息。生產(chǎn)者A很明確這個消息是發(fā)給消費者B的。通常是P2P模式。事件驅(qū)動:生產(chǎn)者A發(fā)出一個事件,消費者B或者消費者C收到這個事件,或者沒人收到這個事件,生產(chǎn)者A只會產(chǎn)生一個事件,不關(guān)心誰會處理這個事件?,通常是發(fā)布-訂閱模型。現(xiàn)代軟件系統(tǒng)是跨多個端點運行并通過大型網(wǎng)絡(luò)連接的分布式系統(tǒng)。例如,考慮一位航空公司客戶通過 Web 瀏覽器購買機票。該訂單可能會通過API,然后通過一系列返回結(jié)果的過程。這些來回通信的一個術(shù)語是消息傳遞。在消息驅(qū)動架構(gòu)中,這些 API 調(diào)用看起來非常像一個函數(shù)調(diào)用:API 知道它在調(diào)用什么,期待某個結(jié)果并等待該結(jié)果。
消息驅(qū)動的優(yōu)點
- 開發(fā)難度低:消息驅(qū)動類似經(jīng)典的編程模型,調(diào)用一個函數(shù),等待一個結(jié)果,對結(jié)果做一些事情,編程簡單快速,開發(fā)難度低。
- 方便調(diào)試維護:因為編程邏輯清晰簡單,流程清晰,調(diào)試起來更加直接方便,后期維護也容易。
常用的消息驅(qū)動框架
API網(wǎng)關(guān)
gRPC
微服務(wù)架構(gòu)
事件驅(qū)動vs消息驅(qū)動
消息驅(qū)動的方法與事件驅(qū)動的方法一樣有很多優(yōu)點和缺點,但每種方法都有自己最適合的情況。
消息感覺很像經(jīng)典的編程模型:調(diào)用一個函數(shù),等待一個結(jié)果,對結(jié)果做一些事情。除了為大多數(shù)程序員所熟悉之外,這種結(jié)構(gòu)還可以使調(diào)試更加直接。另一個優(yōu)點是消息“阻塞”,這意味著呼叫和響應(yīng)的各個單元坐下來等待輪到接收者進(jìn)行處理。事件驅(qū)動系統(tǒng)使單個事件易于隔離測試。然而,這種與整個應(yīng)用系統(tǒng)的分離也抑制了這些單元報告錯誤、重試調(diào)用程序甚至只是向用戶確認(rèn)進(jìn)程已完成的能力。換句話說:當(dāng)事件驅(qū)動系統(tǒng)中發(fā)生錯誤時,很難追蹤到底是哪里出了問題??捎^察性工具正在應(yīng)對調(diào)試復(fù)雜事件鏈的挑戰(zhàn)。但是,添加到業(yè)務(wù)交易交叉點的每個工具都會為負(fù)責(zé)管理這些工作流的程序員帶來另一層復(fù)雜性。如果通信通常以一對一的方式進(jìn)行,并且優(yōu)先接收定期狀態(tài)更新或確認(rèn),那么您將傾向于使用基于消息的方法。但是,如果系統(tǒng)之間的交互特別復(fù)雜,并且確認(rèn)和狀態(tài)更新導(dǎo)致的延遲使得等待它們變得不切實際,那么事件驅(qū)動的設(shè)計可能更合適。但是請記住,大多數(shù)大型組織最終會采用混合策略,一些面向客戶/API 調(diào)用使用消息驅(qū)動,而企業(yè)本身使用事件驅(qū)動。因此,盡可能多地熟悉兩者并沒有什么壞處。
數(shù)據(jù)驅(qū)動
數(shù)據(jù)驅(qū)動核心出發(fā)點是相對于程序邏輯,人類更擅長于處理數(shù)據(jù)。數(shù)據(jù)比程序邏輯更容易駕馭,所以我們應(yīng)該盡可能的將設(shè)計的復(fù)雜度從程序代碼轉(zhuǎn)移至數(shù)據(jù)。例子假設(shè)有一個程序,需要處理其他程序發(fā)送的消息,消息類型是字符串,每個消息都需要一個函數(shù)進(jìn)行處理。第一印象,我們可能會這樣處理:
上面的消息類型取自sip協(xié)議(不完全相同,sip協(xié)議借鑒了http協(xié)議),消息類型可能還會增加。看著常常的流程可能有點累,檢測一下中間某個消息有沒有處理也比較費勁,而且,每增加一個消息,就要增加一個流程分支。
按照數(shù)據(jù)驅(qū)動編程的思路,可能會這樣設(shè)計:
下面這種思路的優(yōu)勢:1、可讀性更強,消息處理流程一目了然。2、更容易修改,要增加新的消息,只要修改數(shù)據(jù)即可,不需要修改流程。3、重用,第一種方案的很多的else if其實只是消息類型和處理函數(shù)不同,但是邏輯是一樣的。下面的這種方案就是將這種相同的邏輯提取出來,而把容易發(fā)生變化的部分提到外面。
隱含在背后的思想很多設(shè)計思路背后的原理其實都是相通的,隱含在數(shù)據(jù)驅(qū)動編程背后的實現(xiàn)思想包括:1、控制復(fù)雜度。通過把程序邏輯的復(fù)雜度轉(zhuǎn)移到人類更容易處理的數(shù)據(jù)中來,從而達(dá)到控制復(fù)雜度的目標(biāo)。2、隔離變化。像上面的例子,每個消息處理的邏輯是不變的,但是消息可能是變化的,那就把容易變化的消息和不容易變化的邏輯分離。3、機制和策略的分離。和第二點很像,本書中很多地方提到了機制和策略。上例中,我的理解,機制就是消息的處理邏輯,策略就是不同的消息處理:
深入理解編程藝術(shù)之策略與機制相分離
數(shù)據(jù)驅(qū)動編程可以用來做什么
- 表驅(qū)動法(Table-Driven)消除重復(fù)代碼,考慮一個消息(事件)驅(qū)動的系統(tǒng),系統(tǒng)的某一模塊需要和其他的幾個模塊進(jìn)行通信。它收到消息后,需要根據(jù)消息的發(fā)送方,消息的類型,自身的狀態(tài),進(jìn)行不同的處理。比較常見的一個做法是用三個級聯(lián)的switch分支實現(xiàn)通過硬編碼來實現(xiàn):
switch(sendMode)
{
case:
}
switch(msgEvent)
{
case:
}
switch(myStatus)
{
case:
}
這種方法的缺點:
- 可讀性不高:找一個消息的處理部分代碼需要跳轉(zhuǎn)多層代碼。
- 過多的switch分支,這其實也是一種重復(fù)代碼。他們都有共同的特性,還? ?可以再進(jìn)一步進(jìn)行提煉。
- 可擴展性差:如果為程序增加一種新的模塊的狀態(tài),這可能要改變所有的? 消息處理的函數(shù),非常的不方便,而且過程容易出錯。
- 程序缺少核心主干:缺少一個能夠提綱挈領(lǐng)的主干,程序的主干被淹沒在? ? 大量的代碼邏輯之中。
用表驅(qū)動法來實現(xiàn)根據(jù)定義的三個枚舉:模塊類型,消息類型,自身模塊狀態(tài),定義一個函數(shù)跳轉(zhuǎn)表:
typedef struct __EVENT_DRIVE
{
MODE_TYPE mod;//消息的發(fā)送模塊
EVENT_TYPE event;//消息類型
STATUS_TYPE status;//自身狀態(tài)
EVENT_FUN eventfun;//此狀態(tài)下的處理函數(shù)指針
}EVENT_DRIVE;
EVENT_DRIVE eventdriver[] = //這就是一張表的定義,不一定是數(shù)據(jù)庫中的表。也可以使自己定義的一個結(jié)構(gòu)體數(shù)組。
{
{MODE_A, EVENT_a, STATUS_1, fun1}
{MODE_A, EVENT_a, STATUS_2, fun2}
{MODE_A, EVENT_a, STATUS_3, fun3}
{MODE_A, EVENT_b, STATUS_1, fun4}
{MODE_A, EVENT_b, STATUS_2, fun5}
{MODE_B, EVENT_a, STATUS_1, fun6}
{MODE_B, EVENT_a, STATUS_2, fun7}
{MODE_B, EVENT_a, STATUS_3, fun8}
{MODE_B, EVENT_b, STATUS_1, fun9}
{MODE_B, EVENT_b, STATUS_2, fun10}
};
int driversize = sizeof(eventdriver) / sizeof(EVENT_DRIVE)//驅(qū)動表的大小
EVENT_FUN GetFunFromDriver(MODE_TYPE mod, EVENT_TYPE event, STATUS_TYPE status)//驅(qū)動表查找函數(shù)
{
int i = 0;
for (i = 0; i < driversize; i )
{
if ((eventdriver[i].mod == mod)