高效異步IO,按我理解起來(lái)應(yīng)該不光要達(dá)到程序運(yùn)行時(shí)的高效,當(dāng)然也要達(dá)到開(kāi)發(fā)效率的高效,其中還包括很重要一點(diǎn)就是質(zhì)量,對(duì)于很多從未做過(guò)異步IO的人來(lái)說(shuō),初次嘗試異步IO肯定會(huì)碰到不少困難,因?yàn)椴还馐菍?duì)編程能力的考驗(yàn),也需要開(kāi)發(fā)者對(duì)操作系統(tǒng)的IO操作有相當(dāng)?shù)牧私猓ㄋ臋C(jī)制和部分原理。異步IO中也有高效低效之分,但主要還是要看具體的應(yīng)用到底需要什么樣機(jī)制。比如大家熟知的select就是個(gè)非常通用且跨平臺(tái)的方法,由于select中需要把大量的時(shí)間花在維護(hù)IO句柄上,導(dǎo)致其效率大打折扣,一般來(lái)說(shuō),對(duì)于小并發(fā)的異步IO操作,比如普通的客戶(hù)端或者是小并發(fā)量的服務(wù)器,它的效率可能也足夠了。關(guān)于select的效率問(wèn)題其實(shí)從各平臺(tái)上對(duì)于FD_SETSIZE的定義就能看出一些來(lái),在windows平臺(tái)上,F(xiàn)D_SETSIZE是64,在Linux平臺(tái)上是1024,也就是說(shuō),對(duì)于平臺(tái)提供商來(lái)說(shuō)也不指望他們提供的select能給你多大的并發(fā)吞吐能力,但由于select的簡(jiǎn)單和普及,其應(yīng)用面還是很廣,很多時(shí)候確實(shí)也不需要太多的并發(fā)量。其實(shí)說(shuō)到高效異步IO的開(kāi)發(fā),我們也說(shuō)了,不光是考慮到程序運(yùn)行時(shí)的效率,還要考慮開(kāi)發(fā)的效率和軟件的質(zhì)量,說(shuō)到這里,其實(shí)select這么個(gè)簡(jiǎn)單的機(jī)制有時(shí)候用起來(lái)也不那么簡(jiǎn)單,而且還會(huì)出很多錯(cuò)誤。
說(shuō)到select重復(fù)的維護(hù)句柄的開(kāi)銷(xiāo),其實(shí)也是有解決方法的,好的解決方法效率會(huì)提高很多,但是重復(fù)工作還是要做的,比如當(dāng)select返回結(jié)果是0,或者當(dāng)能確定不需要增減IO句柄時(shí),我們可以簡(jiǎn)單的把原先保存的FD_SET副本重新寫(xiě)入,這樣可以減少重新生成FD_SET的開(kāi)銷(xiāo),內(nèi)存復(fù)制效率顯然高于隊(duì)列的一次次遍歷,這是顯而易見(jiàn)的。當(dāng)然對(duì)于大并發(fā)量的IO操作來(lái)說(shuō),這種方法對(duì)于效率的提高也是很有限的,說(shuō)到底即使采用異步IO效率也并不一定就高,還要取決于很多其它因素。其中很重要一點(diǎn)大家別忘記把偵聽(tīng)端口設(shè)置成異步的,雖然不設(shè)置的話(huà)程序運(yùn)行后好像沒(méi)有什么不正常,但光從表面上就可以看到CPU占用率明顯偏高,當(dāng)然還會(huì)有一些其它問(wèn)題產(chǎn)生,這個(gè)問(wèn)題比較復(fù)雜,這里不作描述了。并發(fā)操作的效率還取決于“連接-->斷開(kāi)->連接”的頻度,頻繁的“連接-->斷開(kāi)->連接”也會(huì)產(chǎn)生不小的開(kāi)銷(xiāo),當(dāng)然這些開(kāi)銷(xiāo)和之后要講的真正實(shí)現(xiàn)業(yè)務(wù)操作需要的開(kāi)銷(xiāo)比起來(lái)其實(shí)要小得多,而且也是有很多方法可以規(guī)避的,對(duì)于采用不同的異步IO實(shí)現(xiàn)方法也是很不同的,效率差異會(huì)非常大。對(duì)我們來(lái)說(shuō),歸根結(jié)底是根據(jù)不同的模型采用不同的方法來(lái)提高效率,作為一個(gè)異步IO的前端“發(fā)生器”應(yīng)該盡可能避免在自己的工作上消耗過(guò)多的CPU資源,而是盡可能把CPU資源讓給具體的業(yè)務(wù)實(shí)現(xiàn)者。
異步I/O中的Edge-Triggered和Level-Triggered是非常重要的概念;Edge-Triggered字面上理解就是指“邊界觸發(fā)”,說(shuō)的是當(dāng)狀態(tài)變化的時(shí)候觸發(fā),以后如果狀態(tài)一直沒(méi)有變化或沒(méi)有重新要求系統(tǒng)給出通知,將不再通知應(yīng)用程序;Level-Triggered是指“狀態(tài)觸發(fā)”,說(shuō)的是在某種狀態(tài)下觸發(fā),如果一直在這種狀態(tài)下就一直觸發(fā)。兩種觸發(fā)方式各有用途,應(yīng)根據(jù)不同的應(yīng)用采用不同的觸發(fā)方式。select一般默認(rèn)采用的是Level-Triggered,而EPoll既可以采用Edge-Triggered,也可以采用Level-Triggered,默認(rèn)是Level-Triggered,而MS的CPIO按這種定義來(lái)說(shuō)應(yīng)該屬于Edge-Triggered。對(duì)于已經(jīng)封裝好的異步I/O架構(gòu)來(lái)說(shuō),具體采用哪種方式其實(shí)無(wú)傷大雅,因?yàn)闊o(wú)論采用哪種方式,都需要在內(nèi)部都實(shí)現(xiàn)正確了,并且讓使用者不再關(guān)心這種具體的觸發(fā)方式為好。
void EPollReactor::NotifyMeWrite(SOCKET handle, SvcHandler *handler)
{
DIAMON_ASSERT(handle != INVALID_SOCKET);
EPoll_Mod_Handle_Events(handle, EPOLLOUT/* | EPOLLET*/);
}
上述代碼中,就是在每次寫(xiě)完數(shù)據(jù)后需要異步I/O框架再通知應(yīng)用程序關(guān)于寫(xiě)完,其中EPoll_Mod_Handle_Events函數(shù)告訴系統(tǒng)給handle注冊(cè)上EPOLLOUT消息,這樣當(dāng)handle完成寫(xiě)操作后,系統(tǒng)將通知框架寫(xiě)完消息,是否加上EPOLLET完全取決于框架同應(yīng)用者之間的協(xié)議,其實(shí)本質(zhì)上就是框架對(duì)外提供的接口和調(diào)用約定。在Diamon::ACE中,采用的是Level-Triggered方式。
void IOCPReactor::NotifyMeWrite(SOCKET handle, SvcHandler *handler)
{
DIAMON_ASSERT(handle != INVALID_SOCKET);
IOCPSvcHandler *iocphandler = (IOCPSvcHandler *)handler;
iocphandler->event_ &= (~IOCP_EVENT_READ);
iocphandler->event_ |= IOCP_EVENT_WRITE;
}
對(duì)比一下,CPIO中的NotifyMeWrite做的不是通知系統(tǒng),而是告訴異步I/O框架自己接下來(lái)應(yīng)該處理寫(xiě)事件了,而對(duì)系統(tǒng)的觸發(fā)工作完全是交給WSAWrite...之類(lèi)的函數(shù)來(lái)完成的。想想啊,每次調(diào)用WSAWrite...不正是對(duì)系統(tǒng)說(shuō),我給這個(gè)handle注冊(cè)一下寫(xiě)事件啊,下次還通知我,你可以試試不調(diào)用WSAWrite...了,下次肯定收不到寫(xiě)通知了。
同步問(wèn)題是高效異步IO設(shè)計(jì)中非常重要但又常被忽視的問(wèn)題(更不能濫用),不好的同步方法有時(shí)會(huì)限制應(yīng)用層的使用。我曾經(jīng)犯過(guò)這么一個(gè)錯(cuò)誤:select操作和FD_SET的操作是必須順序進(jìn)行的,否則會(huì)產(chǎn)生不可預(yù)期的后果。我們知道,在寫(xiě)數(shù)據(jù)操作后往往需要將寫(xiě)通知告訴應(yīng)用層,因此需要在寫(xiě)操作后往寫(xiě)FD_SET中設(shè)置handle,但是寫(xiě)方法的調(diào)用和select方法的調(diào)用可能是在2個(gè)線(xiàn)程中,當(dāng)我一開(kāi)始遇到這個(gè)問(wèn)題的時(shí)候,我只簡(jiǎn)單的在select的外圍加了一對(duì)mutex操作,當(dāng)應(yīng)用層在收到讀通知中直接調(diào)用寫(xiě)方法沒(méi)有問(wèn)題,因?yàn)檫@時(shí)寫(xiě)方法的調(diào)用和select調(diào)用處在一個(gè)線(xiàn)程中(肯定是順序執(zhí)行的),表面上看這個(gè)問(wèn)題確實(shí)是解決了,但實(shí)際上卻隱藏了一些很?chē)?yán)重的問(wèn)題,當(dāng)寫(xiě)方法是在其他的“業(yè)務(wù)處理器”中,比如另一個(gè)線(xiàn)程中,那么這種調(diào)用就會(huì)導(dǎo)致FD_SET的操作和select操作有同時(shí)進(jìn)行的可能,而當(dāng)這種情況發(fā)生后,顯而易見(jiàn)程序是不可能正常運(yùn)行了。而對(duì)于應(yīng)用程序來(lái)說(shuō),唯一解決這個(gè)問(wèn)題的方法,就是要知道在select的外圍的mutex對(duì)象,然后在自己調(diào)用寫(xiě)方法的外圍再包上一對(duì)這個(gè)mutex操作,雖然解決了FD_SET操作和select操作同步的問(wèn)題,但實(shí)際上卻把問(wèn)題更復(fù)雜化了,比如:一、讓?xiě)?yīng)用程序知道本不需要,更不應(yīng)該讓它知道的邏輯,導(dǎo)致了應(yīng)用層開(kāi)發(fā)的復(fù)雜性,甚至給應(yīng)用程序帶來(lái)由于錯(cuò)誤使用導(dǎo)致更嚴(yán)重的問(wèn)題;二、始終會(huì)存在某幾種情況會(huì)導(dǎo)致互鎖問(wèn)題的產(chǎn)生。第二個(gè)問(wèn)題可能會(huì)有點(diǎn)復(fù)雜,為了便于理解,我給大家解釋一下互鎖,我們考慮這么一種情況,有兩個(gè)任務(wù)分別使用mutexA和mutexB,任務(wù)1中使用的順序是...,mutexA.Lock(),...mutexB(),...,任務(wù)2中使用的順序是...,mutexB.Lock(),...mutexA(),...,當(dāng)任務(wù)1占用mutexA后等待mutexB前,任務(wù)2也剛好占用了mutexB在等待mutexA,這時(shí)候很明顯就制造了一個(gè)互鎖(交叉鎖)。第二個(gè)問(wèn)題其實(shí)就是由這種調(diào)用下導(dǎo)致的某種互鎖情況。總之,我的這種不動(dòng)腦子的解決問(wèn)題的方法導(dǎo)致了嚴(yán)重的應(yīng)用層問(wèn)題。
轉(zhuǎn)自: http://hippoweilin.mobile.spaces.live.com/arc.aspx