上一節(jié)描述了如何使用c語(yǔ)言的函數(shù)指針實(shí)現(xiàn)一個(gè)結(jié)構(gòu)體接口秉犹,從而實(shí)現(xiàn)了類似于面向?qū)ο蟮姆庋b機(jī)制玄帕,多態(tài)性只是其中一個(gè)方便的地方,更為重要的是译秦,它使用消息機(jī)制代替了值的更改,所以我們可以不再使用傳統(tǒng)的獲取狀態(tài)然后賦值的方式來(lái)實(shí)現(xiàn)一個(gè)系統(tǒng),在正常情況下诀浪,我們獲得了抽象機(jī)制棋返,這好像已經(jīng)足夠強(qiáng)大了,但是還不夠雷猪,在當(dāng)今網(wǎng)絡(luò)服務(wù)的興起情況下睛竣,由于并發(fā)系統(tǒng)的流行,我們將看到這樣的抽象方法論的真正威力求摇。
讀者可以復(fù)習(xí)一下之前的文章射沟,因?yàn)檫@個(gè)模型也是使用之前的c語(yǔ)言語(yǔ)法來(lái)實(shí)現(xiàn)。
“這就創(chuàng)建了一種簡(jiǎn)單的時(shí)態(tài)邏輯与境,將“事實(shí)集合”顯現(xiàn)為世界線的層層堆椦楹唬”
首先我們需要一個(gè)并發(fā)執(zhí)行的函數(shù)。
typedef struct shareOBJ shareOBJ;
struct shareOBJ{
int id;
...
};
void pthread(void * obj){
shareOBJ * object = (shareOBJ*)obj;
message * client_message = obj->client_message;
obj->exec(client_message);//獲取客戶端命令并執(zhí)行
}
int main(){
shareOBJ * obj = new_ShareOBJ();//初始化內(nèi)存空間
while(1){
obj->accept();//阻塞等待服務(wù)器消息
pthread_create(NULL,NULL,pthread, (void*)obj)摔刁;//開啟新線程處理
}
}
我們可以看到挥转,現(xiàn)在的邏輯處理是完全封閉的,具體的業(yè)務(wù)是什么共屈?不知道绑谣,內(nèi)部的邏輯是什么,也不知道拗引,我們只知道借宵,現(xiàn)在每到來(lái)一個(gè)請(qǐng)求,我們就開啟一個(gè)線程去處理矾削,其中客戶端消息全部存儲(chǔ)到我們的shareOBJ這個(gè)類型的結(jié)構(gòu)體里面壤玫。
我們暫且按照具體的情況來(lái)處理,假如現(xiàn)在我們?cè)趕hareOBJ結(jié)構(gòu)體內(nèi)部維護(hù)了這樣一組數(shù)據(jù):客戶端的消息:message,賬戶總額:cash哼凯,客戶余額表:table.
{
...
message * msg;
float cash;
table * table;
...
}
通常的情況下欲间,假如來(lái)了一個(gè)客戶要取錢,我們可以這樣處理,將客戶的余額按照他的表中的賬戶進(jìn)行計(jì)算断部,然后將表賦值為更改之后的值括改,由于表中的數(shù)據(jù)在同一時(shí)間內(nèi),一個(gè)客戶只能執(zhí)行對(duì)應(yīng)自己賬戶的一次操作家坎,所以我們的數(shù)據(jù)不會(huì)出現(xiàn)任何問(wèn)題。
//接口1:將客戶的賬戶余額按照客戶指定的存取方式進(jìn)行核算
computeClient(table*tab,message*msg){
tab->data[msg->clientID] = tab->data[msg->clientID] + msg->putgetCash;
}
但是我們同時(shí)還要計(jì)算賬戶的總額吝梅,這個(gè)時(shí)候如果再進(jìn)行更改和賦值虱疏,就會(huì)出現(xiàn)大問(wèn)題了,因?yàn)榭蛻舸嫒∵壿嬒履玫降目傆囝~數(shù)據(jù)是不一樣的苏携,我們具體來(lái)看做瞪。
//接口2:計(jì)算銀行的總額
computeBank(float * cash,message * msg){
*cash = *cash + msg->putgetCash;
}
現(xiàn)在的執(zhí)行邏輯是這樣:
cash0進(jìn)入客戶1,cash0進(jìn)入客戶2,cash0進(jìn)入客戶3......
客戶1存錢或者取錢装蓬,得到了一份拷貝cash1,
客戶2存錢或者取錢著拭,得到了一份拷貝cash2,
客戶3存錢或者取錢,得到了一份拷貝cash3.
我們知道cash1和cash2還有cash3是cash0在不同的客戶邏輯下得到的存取數(shù)目牍帚,而我們每一次得到的數(shù)據(jù)都是并發(fā)執(zhí)行的儡遮,這樣的總額將會(huì)出現(xiàn)奇怪的變化。
假如按照上面的邏輯來(lái)計(jì)算暗赶,我們下一次得到的必定是cash1,現(xiàn)在我們的第四個(gè)客戶來(lái)了鄙币,他是我們的銀行老板,使用cash1結(jié)算后的銀行總額來(lái)查帳本,卻發(fā)現(xiàn)蹂随,明明有3個(gè)人來(lái)存了錢十嘿,但是現(xiàn)金庫(kù)里和賬戶點(diǎn)算出來(lái)只有客戶1一個(gè)人存了錢,導(dǎo)致我們的現(xiàn)金還足余岳锁,銀行賬本上已經(jīng)沒(méi)錢了绩衷,難道是因?yàn)榭蛻?和3的錢還沒(méi)有打到賬上嗎?仔細(xì)想想激率,我們無(wú)論哪一個(gè)人先存錢咳燕,賬本都是按照之前的那個(gè)沒(méi)有更新的賬本在計(jì)算,并且將任意一個(gè)人的計(jì)算結(jié)果放到了總的余額中柱搜,所以導(dǎo)致了這樣的情況迟郎。
出現(xiàn)問(wèn)題的根本原因就是我們采用了并發(fā)的策略,而不是排隊(duì)的方式來(lái)處理我們的計(jì)算過(guò)程聪蘸,每一個(gè)上下文引用后的變量都是分離的宪肖,最終又把在時(shí)間中丟失一部分狀態(tài)后的這些操作直接安裝到原來(lái)的時(shí)間線上,我們完全可以使用排隊(duì)的方式處理這個(gè)問(wèn)題健爬,從一個(gè)客戶進(jìn)入處理流程控乾,再到結(jié)束辦理,我們?cè)賵?zhí)行等待處理下一個(gè)客戶的業(yè)務(wù)娜遵,但是這樣可能會(huì)頻繁切換上下文蜕衡,導(dǎo)致的性能開銷也是及其昂貴的,甚至是無(wú)法忍受的设拟,因?yàn)橐粋€(gè)業(yè)務(wù)流程中慨仿,最消耗時(shí)間的并不是對(duì)余額進(jìn)行計(jì)算,而是對(duì)客戶數(shù)據(jù)的計(jì)算纳胧,而我們的客戶數(shù)據(jù)是可以并發(fā)執(zhí)行沒(méi)有問(wèn)題的镰吆,所以一個(gè)可行的解決方案就是將客戶數(shù)據(jù)的業(yè)務(wù)推入并發(fā)流程中,而余額等一系列共享變量的計(jì)算使用循環(huán)等待的方式解決跑慕,但是這仍然具有很大的性能消耗万皿,有沒(méi)有一種辦法既可以不用頻繁切換上下文摧找,又可以把丟失的那部分未經(jīng)計(jì)算的狀態(tài)找回來(lái)呢?當(dāng)然是有的牢硅,那就是我們的惰性計(jì)算蹬耘。
惰性計(jì)算的好處
惰性計(jì)算其實(shí)理解起來(lái)很簡(jiǎn)單,就是將原本時(shí)間線上本應(yīng)立即計(jì)算的一個(gè)對(duì)象或者狀態(tài)延遲一段時(shí)間再進(jìn)行計(jì)算减余,這往往在函數(shù)式編程中利用它來(lái)實(shí)現(xiàn)無(wú)窮數(shù)列這樣的數(shù)據(jù)結(jié)構(gòu)综苔,又叫做流,在當(dāng)年那個(gè)計(jì)算機(jī)性能極度缺乏的情況下佳励,這是一種魔法般的武器休里,不過(guò)到了今天,這樣的抽象方法論更多地用在并發(fā)環(huán)境下方便的地操作數(shù)據(jù)同步和解決鎖機(jī)制帶來(lái)的問(wèn)題赃承。
那么實(shí)現(xiàn)的關(guān)鍵步驟在于妙黍,我們將結(jié)構(gòu)體shareOBJ中的固定值 float cash 更改為可以存儲(chǔ)當(dāng)前值的歷史的一個(gè)棧,我們客戶端的更改操作可以設(shè)置為向棧取出一個(gè)當(dāng)前時(shí)間幀上的真實(shí)數(shù)據(jù)瞧剖,并向其中存入一個(gè)更改后的歷史值拭嫁,也就是說(shuō),無(wú)論我們的時(shí)間幀有多么小抓于,取到的數(shù)據(jù)都是在時(shí)間上同步后的數(shù)據(jù)做粤,因?yàn)槲覀兊臈⑺械母挠涗浂急4嫦聛?lái)了,現(xiàn)在關(guān)鍵的地方到了捉撮,我們從哪里尋找這樣的棧呢怕品?它必須要安裝所有的歷史記錄才可以,好像我們走到了一個(gè)想當(dāng)然的地步巾遭,這時(shí)候我們的惰性計(jì)算出場(chǎng)了肉康,我們完全可以不必提前得到一個(gè)無(wú)窮大的棧,而只需要在用到下一個(gè)椬粕幔空間的時(shí)候我們?cè)偃?gòu)造它吼和,聽起來(lái)好像理所當(dāng)然,我們用到的下一個(gè)椘锼兀空間肯定是構(gòu)造出來(lái)的呀炫乓,可是總會(huì)用完這個(gè)棧,很多人可能會(huì)想到一個(gè)辦法献丑,將這塊內(nèi)存作為緩沖區(qū)末捣,然后棧滿的時(shí)候就可以持久化,其實(shí)這是一個(gè)辦法创橄,但是還有更優(yōu)雅的解決方法塔粒,因?yàn)槲覀冎魂P(guān)心最終的余額,所以這些實(shí)時(shí)的計(jì)算過(guò)程可以另啟一個(gè)余額計(jì)算函數(shù)筐摘,每隔特定的時(shí)間幀或者滿足棧滿的觸發(fā)條件就對(duì)余額進(jìn)行核算,并且將棧清空,等待下一輪的訪問(wèn)高峰咖熟,因?yàn)槲覀兊臈?nèi)元素假如為滿圃酵,是無(wú)法再添加元素的,這時(shí)候可以返回失敗條件馍管,但是我們的余額計(jì)算函數(shù)速度是很快的郭赐,幾乎不會(huì)占用多少時(shí)間,反而是我們的客戶數(shù)據(jù)的計(jì)算還有日志存儲(chǔ)占用了太多的時(shí)間确沸,這個(gè)時(shí)候我們可以將消息記錄中的所有的總余額數(shù)據(jù)全部寫入磁盤進(jìn)行持久化捌锭,客戶端的操作什么時(shí)候可以返回成功條件呢?當(dāng)我們的余額計(jì)算時(shí)間片輪轉(zhuǎn)一次就返回一次成功消息罗捎,因?yàn)檩嗈D(zhuǎn)一次代表我們已經(jīng)處理了一輪時(shí)間剛好滿足的訪問(wèn)操作观谦,只要滿足任意一個(gè)條件我們就可以使用余額計(jì)算函數(shù)返回客戶端的操作消息。
看起來(lái)我們的計(jì)算函數(shù)成功了桨菜,因?yàn)樗挠?jì)算不再是模擬現(xiàn)實(shí)世界的時(shí)間線豁状,我們的用戶操作并不會(huì)立即得到計(jì)算結(jié)果,而是在我們?cè)O(shè)計(jì)者已經(jīng)規(guī)定的條件下才得到結(jié)果倒得,比如棧滿或者時(shí)間片輪轉(zhuǎn)的時(shí)候泻红,這就是一種解耦,將真實(shí)世界的時(shí)間線解耦到棧中霞掺,只要我們擁有完整的消息數(shù)據(jù)谊路,我們完全可以選擇任意時(shí)刻計(jì)算得到我們想要的結(jié)果,這也是函數(shù)式編程的思想菩彬,引用透明性缠劝,我們不需要模擬現(xiàn)實(shí)世界的時(shí)間走向,可以延遲到任意的時(shí)間去計(jì)算也不會(huì)產(chǎn)生改變挤巡,這便是時(shí)態(tài)邏輯剩彬,某些事實(shí)在任意時(shí)間都成立,無(wú)論等一千年還是一萬(wàn)年去把存取值的歷史棧取出來(lái)計(jì)算矿卑,我們最終得到的值仍然是一樣的喉恋,如果在并發(fā)中訪問(wèn)固定的變量和狀態(tài)變化的話,我們將會(huì)得到很可怕的結(jié)果母廷,因?yàn)槟承┲翟跁r(shí)間中消失了轻黑!
{
stack * cash;
}
stack.setValue();//實(shí)際上此處的setValue已經(jīng)代替賦值操作
stack.time();//time 函數(shù)會(huì)計(jì)算當(dāng)前的時(shí)延,超過(guò)響應(yīng)時(shí)間就將進(jìn)行計(jì)算琴昆,并清空棧
stack.push();//push會(huì)將新的消息傳入棧
stack.compute();//計(jì)算函數(shù)會(huì)立即鎖定棧氓鄙,并計(jì)算所有的棧元素相加得到一個(gè)穩(wěn)定計(jì)算后的值,然后清空棧
stack.toNULL();//toNULL立即釋放棧的空間业舍,并解除鎖定抖拦,接收客戶端請(qǐng)求升酣。
看起來(lái)我們的操作好像顯得很復(fù)雜,但是實(shí)際上這樣的設(shè)計(jì)完美地避開了不穩(wěn)定狀態(tài)帶來(lái)的并發(fā)問(wèn)題态罪,并且我們只在執(zhí)行緩沖更換操作的時(shí)候通過(guò)內(nèi)部狀態(tài)的機(jī)制給定了一把不完全是鎖的鎖噩茄,因?yàn)樗臓顟B(tài)完全由對(duì)象本身來(lái)決定,而不是外部手動(dòng)去操作复颈,自然不會(huì)存在競(jìng)爭(zhēng)條件和死鎖等問(wèn)題绩聘,并且它的速度和安全性都遠(yuǎn)遠(yuǎn)高于使用鎖機(jī)制,當(dāng)我們?cè)趐ush的時(shí)候耗啦,我們的對(duì)象會(huì)自己內(nèi)省是否在進(jìn)行計(jì)算操作凿菩,否則就返回棧滿條件,外部邏輯是執(zhí)行循環(huán)等待還是直接返回錯(cuò)誤消息完全取決于外部的系統(tǒng)條件帜讲,push更好的方式甚至可以是內(nèi)部循環(huán)等待衅谷,外部的操作是無(wú)法觀察到當(dāng)前線程引用stack的push操作到底發(fā)生了什么,所以我們的并發(fā)系統(tǒng)成功運(yùn)行了舒帮,當(dāng)然這個(gè)設(shè)計(jì)還只是作者的一點(diǎn)思考会喝。
下面是具體的邏輯代碼
setValue(){
if(cash.time()==0.2 || cash.state == LOCK){
stack.compute()
stack.unlock()
}
else{
stack.push()
}
}
computeBank(stack * cash,message * msg){
cash.setValue(msg->putgetCash);
}