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