應(yīng)用程序:一個異步的HTTP服務(wù)器的設(shè)計 假設(shè)我們要設(shè)計一個HTTP服務(wù)器,它的設(shè)計目標(biāo)包括:高并發(fā)性、精簡(部分支持HTTP/1.1)、支持plug-in結(jié)構(gòu)。在不少場合可能都有這個需求??傮w上來說,HTTP服務(wù)器可以類比成一個基于多線程的操作系統(tǒng):OS調(diào)度每個工作線程在適當(dāng)?shù)臅r候獲得執(zhí)行,而工作線程提供服務(wù)(也就是處理HTTP請求)。在這個基礎(chǔ)上,主要的考慮就是調(diào)度粒度的大小,粒度太大的時候并發(fā)性會降低,而粒度太小又可能因為任務(wù)切換(考慮OS的Context Switching)而導(dǎo)致效率降低,所以這又是一個折衷的結(jié)果。類似于Apache(以及其他的HTTP服務(wù)器),我們可以把一個HTTP處理過程分為若干個狀態(tài),基于這些狀態(tài)可以構(gòu)造出一個HTTP處理的狀態(tài)機。這種情況下,我們就可以把每個狀態(tài)的處理作為調(diào)度的粒度。一個調(diào)度過程就是:一個工作線程從全局的任務(wù)隊列里取出一個HTTP_Context結(jié)構(gòu);根據(jù)當(dāng)前的狀態(tài)完成相應(yīng)處理;然后根據(jù)狀態(tài)機設(shè)置下一個狀態(tài);再放回到全局的任務(wù)隊列里。這樣子,若干個HTTP狀態(tài)就可以通過這個調(diào)度策略構(gòu)成一個完整HTTP處理過程。顯而易見,一個狀態(tài)對于下一個狀態(tài)處理的調(diào)用都可以認(rèn)為是異步的。一個HTTP狀態(tài)機的設(shè)計如下圖所示。
工作線程的函數(shù)其實就是兩個操作:從狀態(tài)隊列里取出一個HTTP_Context,調(diào)用HTTP_Context的service()函數(shù),周而復(fù)此。在這個架構(gòu)上,就很容易引入異步I/O和Plug-in的機制了。事實上我們也可以使用基于事件(例如select/poll)的I/O策略來模擬異步I/O,實現(xiàn)中使用一個tb用戶線程就可以了。
對于異步I/O和Plug-in的調(diào)用,我們也是采用類似于Linux 2.6里面aio的重試方案,而異步完成的時候采用回調(diào)函數(shù)。在某個狀態(tài)上,如果系統(tǒng)需要I/O操作(recv或者send),則會請求一個異步I/O(操作系統(tǒng)提供的異步I/O或者由用戶線程模擬的異步I/O),這時候相應(yīng)的HTTP_Context不會重新回到狀態(tài)隊列里,而在I/O完成的回調(diào)函數(shù)里面才會重新放回到狀態(tài)隊列,得到重新調(diào)度的機會。HTTP_Context得到重新調(diào)度的時候會檢查I/O狀態(tài)(這個可以通過一些標(biāo)志位來完成),如果已經(jīng)完成,則處理然后設(shè)置下一狀態(tài),重新調(diào)度,否則可以重新請求一個新的I/O請求。Plug-in也可以使用類似的方案,比如說一個Plug-in要跟外部的一個服務(wù)器通信,這時候就可以在通信完成的時候才把HTTP_Context重新放回到狀態(tài)隊列。顯然,Plug-in跟HTTP狀態(tài)是多對多的關(guān)系,一個Plug-in可以在若干個關(guān)心的狀態(tài)注冊自身,同時還可以設(shè)置一些short-path來提高處理的效率。
結(jié)論 總的來說,異步調(diào)用的設(shè)計和應(yīng)用歸根結(jié)底就是對多個主動對象的管理問題:如何提供執(zhí)行的動力以及如何保證執(zhí)行的順序邏輯。主要考慮的問題是主動對象的粒度以及執(zhí)行方式,同步或者回調(diào)來完成順序的調(diào)度,或者使用近似的調(diào)度而加一些魯棒的錯誤處理機制來保證語義的正確。后者可以考慮在使用基于事件的socket的時候,readable事件的通知可以是冗余的,或者說可以比實際中發(fā)生的readable事件更多,這個時候使用非阻塞的socket,有些read()(或者recv())會直接返回EWOULDBLOCK,系統(tǒng)只要考慮處理這種情況(使用non blocking socket而不是blocking socket),當(dāng)例外的情況不多的時候是可以接受的。這時候可以說事件的報告就只是近似的。