本文主要探討一下windows平臺(tái)上的完成端口開(kāi)發(fā)及其與之相關(guān)的幾個(gè)重要的技術(shù)概念,這些概念都是與基于IOCP的開(kāi)發(fā)密切相關(guān)的,對(duì)開(kāi)發(fā)人員來(lái)講,又不得不給予足夠重視的幾個(gè)概念:
1) 基于IOCP實(shí)現(xiàn)的服務(wù)吞吐量
2)IOCP模式下的線程切換
3)基于IOCP實(shí)現(xiàn)的消息的亂序問(wèn)題。
一、IOCP簡(jiǎn)介
提到IOCP,大家都非常熟悉,其基本的編程模式,我就不在這里展開(kāi)了。在這里我主要是把IOCP中所提及的概念做一個(gè)基本性的總結(jié)。IOCP的基本架構(gòu)圖如下:
如圖所示:在IOCP中,主要有以下的參與者:
--》完成端口:是一個(gè)FIFO隊(duì)列,操作系統(tǒng)的IO子系統(tǒng)在IO操作完成后,會(huì)把相應(yīng)的IO packet放入該隊(duì)列。
--》等待者線程隊(duì)列:通過(guò)調(diào)用GetQueuedCompletionStatus API,在完成端口上等待取下一個(gè)IO packet。
--》執(zhí)行者線程組:已經(jīng)從完成端口上獲得IO packet,在占用CPU進(jìn)行處理。
除了以上三種類型的參與者。我們還應(yīng)該注意兩個(gè)關(guān)聯(lián)關(guān)系,即:
--》IO Handle與完成端口相關(guān)聯(lián):任何期望使用IOCP的方式來(lái)處理IO請(qǐng)求的,必須將相應(yīng)的IO Handle與該完成端口相關(guān)聯(lián)。需要指出的時(shí),這里的IO Handle,可以是File的Handle,或者是Socket的Handle。
--》線程與完成端口相關(guān)聯(lián):任何調(diào)用GetQueuedCompletionStatus API的線程,都將與該完成端口相關(guān)聯(lián)。在任何給定的時(shí)候,該線程只能與一個(gè)完成端口相關(guān)聯(lián),與最后一次調(diào)用的GetQueuedCompletionStatus為準(zhǔn)。
二、高并發(fā)的服務(wù)器(基于socket)實(shí)現(xiàn)方法
一般來(lái)講,實(shí)現(xiàn)基于socket的服務(wù)器,有三種實(shí)現(xiàn)的方式(thread per request的方式,我就不提了:)):
第一、線程池的方式。使用線程池來(lái)對(duì)客戶端請(qǐng)求進(jìn)行服務(wù)。使用這種方式時(shí),當(dāng)客戶端對(duì)服務(wù)器的連接是短連接(所謂的短連接,即:客戶端對(duì)服務(wù)器不是長(zhǎng)時(shí)間連接)時(shí),是可以考慮的。但是,如若客戶端對(duì)服務(wù)器的連接是長(zhǎng)連接時(shí),我們需要限制服務(wù)器端的最大連接數(shù)目為線程池線程的最大數(shù)目,而這應(yīng)用的設(shè)計(jì)本身來(lái)講,是不好的設(shè)計(jì)方式,scalability會(huì)存在問(wèn)題。
第二、基于Select的服務(wù)器實(shí)現(xiàn)。其本質(zhì)是,使用Select(操作系統(tǒng)提供的API)來(lái)監(jiān)視連接是否可讀,可寫(xiě),或者是否出錯(cuò)。相比于前一種方式,Select允許應(yīng)用使用一個(gè)線程(或者是有限幾個(gè)線程)來(lái)監(jiān)視連接的可讀寫(xiě)性。當(dāng)有連接可讀可寫(xiě)時(shí),應(yīng)用可以以non-bolock的方式讀寫(xiě)socket上的數(shù)據(jù)。使用Select的方式的缺點(diǎn)是,當(dāng)Select所監(jiān)視的連接數(shù)目在千的數(shù)量級(jí)時(shí),性能會(huì)打折扣。這是因?yàn)椴僮飨到y(tǒng)內(nèi)核需要在內(nèi)部對(duì)這些Socket進(jìn)行輪詢,以檢查其可讀寫(xiě)性。另一個(gè)問(wèn)題是:應(yīng)用必須在處理完所有的可讀寫(xiě)socket的IO請(qǐng)求之后,才能再次調(diào)用Select,進(jìn)行下一輪的檢查,否則會(huì)有潛在的問(wèn)題。這樣,造成的結(jié)果是,對(duì)一些請(qǐng)求的處理會(huì)出現(xiàn)饑餓的現(xiàn)象。
一般common的做法是Select結(jié)合Leader-Follower設(shè)計(jì)模式使用。不過(guò)不管怎樣,Select的本質(zhì)造成了其在Scalability的問(wèn)題是不如IOCP,這也是很多high-scalabe的服務(wù)器采用IOCP的原因。
第三、IOCP實(shí)現(xiàn)高并發(fā)的服務(wù)器。IOCP是實(shí)現(xiàn)high-scalabe的服務(wù)器的首選。其特點(diǎn)我們專門(mén)在下一小姐陳述。
三、IOCP開(kāi)發(fā)的幾個(gè)概念
第一、服務(wù)器的吞吐量問(wèn)題。
我們都知道,基于IOCP的開(kāi)發(fā)是異步IO的,也正是這一技術(shù)的本質(zhì),決定了IOCP所實(shí)現(xiàn)的服務(wù)器的高吞吐量。
我們舉一個(gè)及其簡(jiǎn)化的例子,來(lái)說(shuō)明這一問(wèn)題。在網(wǎng)絡(luò)服務(wù)器的開(kāi)發(fā)過(guò)程中,影響其性能吞吐量的,有很多因素,在這里,我們只是把關(guān)注點(diǎn)放在兩個(gè)方面,即:網(wǎng)絡(luò)IO速度與Disk IO速度。我們假設(shè):在一個(gè)千兆的網(wǎng)絡(luò)環(huán)境下,我們的網(wǎng)絡(luò)傳輸速度的極限是大概125M/s,而Disk IO的速度是10M/s。在這樣的前提下,慢速的Disk 設(shè)備會(huì)成為我們整個(gè)應(yīng)用的瓶頸。我們假設(shè)線程A負(fù)責(zé)從網(wǎng)絡(luò)上讀取數(shù)據(jù),然后將這些數(shù)據(jù)寫(xiě)入Disk。如果對(duì)Disk的寫(xiě)入是同步的,那么線程A在等待寫(xiě)完Disk的過(guò)程是不能再?gòu)木W(wǎng)絡(luò)上接受數(shù)據(jù)的,在寫(xiě)入Disk的時(shí)間內(nèi),我們可以認(rèn)為這時(shí)候Server的吞吐量為0(沒(méi)有接受新的客戶端請(qǐng)求)。對(duì)于這樣的同步讀寫(xiě)Disk,一些的解決方案是通過(guò)增加線程數(shù)來(lái)增加服務(wù)器處理的吞吐量,即:當(dāng)線程A從網(wǎng)絡(luò)上接受數(shù)據(jù)后,驅(qū)動(dòng)另外單獨(dú)的線程來(lái)完成讀寫(xiě)Disk任務(wù)。這樣的方案缺點(diǎn)是:需要線程間的合作,需要線程間的切換(這是另一個(gè)我們要討論的問(wèn)題)。而IOCP的異步IO本質(zhì),就是通過(guò)操作系統(tǒng)內(nèi)核的支持,允許線程A以非阻塞的方式向IO子系統(tǒng)投遞IO請(qǐng)求,而后馬上從網(wǎng)絡(luò)上讀取下一個(gè)客戶端請(qǐng)求。這樣,結(jié)果是:在不增加線程數(shù)的情況下,IOCP大大增加了服務(wù)器的吞吐量。說(shuō)到這里,聽(tīng)起來(lái)感覺(jué)很像是DMA。的確,許多軟件的實(shí)現(xiàn)技術(shù),在本質(zhì)上,與硬件的實(shí)現(xiàn)技術(shù)是相通的。另外一個(gè)典型的例子是硬件的流水線技術(shù),同樣,在軟件領(lǐng)域,也有很著名的應(yīng)用。好像話題扯遠(yuǎn)了,呵呵:)
第二、線程間的切換問(wèn)題。
服務(wù)器的實(shí)現(xiàn),通過(guò)引入IOCP,會(huì)大大減少Thread切換帶來(lái)的額外開(kāi)銷。我們都知道,對(duì)于服務(wù)器性能的一個(gè)重要的評(píng)估指標(biāo)就是:System\Context Switches,即單位時(shí)間內(nèi)線程的切換次數(shù)。如果在每秒內(nèi),線程的切換次數(shù)在千的數(shù)量級(jí)上,這就意味著你的服務(wù)器性能值得商榷。Context Switches/s應(yīng)該越小越好。說(shuō)到這里,我們來(lái)重新審視一下IOCP。
完成端口的線程并發(fā)量可以在創(chuàng)建該完成端口時(shí)指定(即NumberOfConcurrentThreads參數(shù))。該并發(fā)量限制了與該完成端口相關(guān)聯(lián)的可運(yùn)行線程的數(shù)目(就是前面我在IOCP簡(jiǎn)介中提到的執(zhí)行者線程組的最大數(shù)目)。當(dāng)與該完成端口相關(guān)聯(lián)的可運(yùn)行線程的總數(shù)目達(dá)到了該并發(fā)量,系統(tǒng)就會(huì)阻塞任何與該完成端口相關(guān)聯(lián)的后續(xù)線程的執(zhí)行,直到與該完成端口相關(guān)聯(lián)的可運(yùn)行線程數(shù)目下降到小于該并發(fā)量為止。最有效的假想是發(fā)生在有完成包在隊(duì)列中等待,而沒(méi)有等待被滿足,因?yàn)榇藭r(shí)完成端口達(dá)到了其并發(fā)量的極限。此時(shí),一個(gè)正在運(yùn)行中的線程調(diào)用GetQueuedCompletionStatus時(shí),它就會(huì)立刻從隊(duì)列中取走該完成包。這樣就不存在著環(huán)境的切換,因?yàn)樵撎幱谶\(yùn)行中的線程就會(huì)連續(xù)不斷地從隊(duì)列中取走完成包,而其他的線程就不能運(yùn)行了。
完成端口的線程并發(fā)量的建議值就是你系統(tǒng)CPU的數(shù)目。在這里,要區(qū)分清楚的是,完成端口的線程并發(fā)量與你為完成端口創(chuàng)建的工作者線程數(shù)是沒(méi)有任何關(guān)系的,工作者線程數(shù)的數(shù)目,完全取決于你的整個(gè)應(yīng)用的設(shè)計(jì)(當(dāng)然這個(gè)不宜過(guò)大,否則失去了IOCP的本意:))。
第三、IOCP開(kāi)發(fā)過(guò)程中的消息亂序問(wèn)題。
使用IOCP開(kāi)發(fā)的問(wèn)題在于它的復(fù)雜。我們都知道,在使用TCP時(shí),TCP協(xié)議本身保證了消息傳遞的次序性,這大大降低了上層應(yīng)用的復(fù)雜性。但是當(dāng)使用IOCP時(shí),問(wèn)題就不再那么簡(jiǎn)單。如下例:
三個(gè)線程同時(shí)從IOCP中讀取Msg1, Msg2,與Msg3。由于TCP本身消息傳遞的有序性,所以,在IOCP隊(duì)列內(nèi),Msg1-Msg2-Msg3保證了有序性。三個(gè)線程分別從IOCP中取出Msg1,Msg2與Msg3,然后三個(gè)線程都會(huì)將各自取到的消息投遞到邏輯層處理。在邏輯處理層的實(shí)現(xiàn),我們不應(yīng)該假定Msg1-Msg2-Msg3順序,原因其實(shí)很簡(jiǎn)單,在Time 1~Time 2的時(shí)間段內(nèi),三個(gè)線程被操作系統(tǒng)調(diào)度的先后次序是不確定的,所以在到達(dá)邏輯處理層,
Msg1,Msg2與Msg3的次序也就是不確定的。所以,邏輯處理層的實(shí)現(xiàn),必須考慮消息亂序的情況,必須考慮多線程環(huán)境下的程序?qū)崿F(xiàn)。
在這里,我把消息亂序的問(wèn)題單列了出來(lái)。其實(shí)在IOCP的開(kāi)發(fā)過(guò)程中,相比于同步的方式,應(yīng)該還有其它更多的難題需要解決,這也是與Select方式相比,IOCP的缺點(diǎn),實(shí)現(xiàn)復(fù)雜度高。
結(jié)束語(yǔ):
ACE的Proactor Framework, 對(duì)windows平臺(tái)的IOCP做了基于Proactor設(shè)計(jì)模式的,面向?qū)ο蟮姆庋b,這在一定程度上簡(jiǎn)化了應(yīng)用開(kāi)發(fā)的難度,是一個(gè)很好的異步IO的開(kāi)發(fā)框架,推薦學(xué)習(xí)使用。
Reference:
Microsoft Technet,Inside I/O Completion Ports