printf("老鐵,感謝有緣一起學(xué)習(xí)網(wǎng)絡(luò)編程,除了跟著這套課程走,為了更好地解決大家實(shí)操中的問題,可以加QQ群676593534,及時(shí)交流。群里還有很多精致資料哦,我這這里等你!");
網(wǎng)絡(luò)編程學(xué)習(xí)路線,請移步這里。
1 同步與異步
首先來解釋同步和異步的概念,這兩個概念與消息的通知機(jī)制有關(guān)。也就是同步與異步主要是從消息通知機(jī)制角度來說的。
1.1 概念描述
所謂同步就是一個任務(wù)的完成需要依賴另外一個任務(wù)時(shí),只有等待被依賴的任務(wù)完成后,依賴的任務(wù)才能算完成,這是一種可靠的任務(wù)序列。要么成功都成功,失敗都失敗,兩個任務(wù)的狀態(tài)可以保持一致。
所謂異步是不需要等待被依賴的任務(wù)完成,只是通知被依賴的任務(wù)要完成什么工作,依賴的任務(wù)也立即執(zhí)行,只要自己完成了整個任務(wù)就算完成了。至于被依賴的任務(wù)最終是否真正完成,依賴它的任務(wù)無法確定,所以它是不可靠的任務(wù)序列。
1.2 消息通知
異步的概念和同步相對。當(dāng)一個同步調(diào)用發(fā)出后,調(diào)用者要一直等待返回消息(結(jié)果)通知后,才能進(jìn)行后續(xù)的執(zhí)行;當(dāng)一個異步過程調(diào)用發(fā)出后,調(diào)用者不能立刻得到返回消息(結(jié)果)。實(shí)際處理這個調(diào)用的部件在完成后,通過狀態(tài)、通知和回調(diào)來通知調(diào)用者。
這里提到執(zhí)行部件和調(diào)用者通過三種途徑返回結(jié)果:狀態(tài)、通知和回調(diào)。使用哪一種通知機(jī)制,依賴于執(zhí)行部件的實(shí)現(xiàn),除非執(zhí)行部件提供多種選擇,否則不受調(diào)用者控制。
如果執(zhí)行部件用狀態(tài)來通知,那么調(diào)用者就需要每隔一定時(shí)間檢查一次,效率就很低(有些初學(xué)多線程編程的人,總喜歡用一個循環(huán)去檢查某個變量的值,這其實(shí)是一種很嚴(yán)重的錯誤);
如果是使用通知的方式,效率則很高,因?yàn)閳?zhí)行部件幾乎不需要做額外的操作。至于回調(diào)函數(shù),其實(shí)和通知沒太多區(qū)別。
1.2 場景比喻
舉個例子,比如我去銀行辦理業(yè)務(wù),可能會有兩種方式:
選擇排隊(duì)等候;
另種選擇取一個小紙條上面有我的號碼,等到排到我這一號時(shí)由柜臺的人通知我輪到我去辦理業(yè)務(wù)了;
第一種:前者(排隊(duì)等候)就是同步等待消息通知,也就是我要一直在等待銀行辦理業(yè)務(wù)情況;
第二種:后者(等待別人通知)就是異步等待消息通知。在異步消息處理中,等待消息通知者(在這個例子中就是等待辦理業(yè)務(wù)的人)往往注冊一個回調(diào)機(jī)制,在所等待的事件被觸發(fā)時(shí)由觸發(fā)機(jī)制(在這里是柜臺的人)通過某種機(jī)制(在這里是寫在小紙條上的號碼,喊號)找到等待該事件的人。
2 阻塞與非阻塞
阻塞和非阻塞這兩個概念與程序(線程)等待消息通知(無所謂同步或者異步)時(shí)的狀態(tài)有關(guān)。也就是說阻塞與非阻塞主要是程序(線程)等待消息通知時(shí)的狀態(tài)角度來說的。
2.1 概念描述
阻塞調(diào)用是指調(diào)用結(jié)果返回之前,當(dāng)前線程會被掛起,一直處于等待消息通知,不能夠執(zhí)行其他業(yè)務(wù)。函數(shù)只有在得到結(jié)果之后才會返回。
有人也許會把阻塞調(diào)用和同步調(diào)用等同起來,實(shí)際上它們是不同的。
對于同步調(diào)用來說,很多時(shí)候當(dāng)前線程可能還是激活的,只是從邏輯上當(dāng)前函數(shù)沒有返回而已,此時(shí),這個線程可能也會處理其他的消息。還有一點(diǎn),在這里先擴(kuò)展下:
(a) 如果這個線程在等待當(dāng)前函數(shù)返回時(shí),仍在執(zhí)行其他消息處理,那這種情況就叫做同步非阻塞;
(b) 如果這個線程在等待當(dāng)前函數(shù)返回時(shí),沒有執(zhí)行其他消息處理,而是處于掛起等待狀態(tài),那這種情況就叫做同步阻塞;
所以同步的實(shí)現(xiàn)方式會有兩種:同步阻塞、同步非阻塞;同理,異步也會有兩種實(shí)現(xiàn):異步阻塞、異步非阻塞;
對于阻塞調(diào)用來說,則當(dāng)前線程就會被掛起等待當(dāng)前函數(shù)返回;
非阻塞和阻塞的概念相對應(yīng),指在不能立刻得到結(jié)果之前,該函數(shù)不會阻塞當(dāng)前線程,而會立刻返回。雖然表面上看非阻塞的方式可以明顯的提高CPU的利用率,但是也帶了另外一種后果就是系統(tǒng)的線程切換增加。增加的CPU執(zhí)行時(shí)間能不能補(bǔ)償系統(tǒng)的切換成本需要好好評估。
2.2 場景比喻
繼續(xù)上面的那個例子,不論是排隊(duì)還是使用號碼等待通知,如果在這個等待的過程中,等待者除了等待消息通知之外不能做其它的事情,那么該機(jī)制就是阻塞的,表現(xiàn)在程序中,也就是該程序一直阻塞在該函數(shù)調(diào)用處不能繼續(xù)往下執(zhí)行。
相反,有的人喜歡在銀行辦理這些業(yè)務(wù)的時(shí)候一邊打打電話發(fā)發(fā)短信一邊等待,這樣的狀態(tài)就是非阻塞的,因?yàn)樗?等待者)沒有阻塞在這個消息通知上,而是一邊做自己的事情一邊等待。
但是需要注意了,同步非阻塞形式實(shí)際上是效率低下的,想象一下你一邊打著電話一邊還需要抬頭看到底隊(duì)伍排到你了沒有。如果把打電話和觀察排隊(duì)的位置看成是程序的兩個操作的話,這個程序需要在這兩種不同的行為之間來回的切換,效率可想而知是低下的;而異步非阻塞形式卻沒有這樣的問題,因?yàn)榇螂娫捠悄?等待者)的事情,而通知你則是柜臺(消息觸發(fā)機(jī)制)的事情,程序沒有在兩種不同的操作中來回切換。
3 同步/異步與阻塞/非阻塞
同步阻塞形式效率是最低的,拿上面的例子來說,就是你專心排隊(duì),什么別的事都不做。
實(shí)際程序中:就是未對fd 設(shè)置O_NONBLOCK標(biāo)志位的read/write 操作;
異步阻塞形式
如果在銀行等待辦理業(yè)務(wù)的人采用的是異步的方式去等待消息被觸發(fā)(通知),也就是領(lǐng)了一張小紙條,假如在這段時(shí)間里他不能離開銀行做其它的事情,那么很顯然,這個人被阻塞在了這個等待的操作上面;
異步操作是可以被阻塞住的,只不過它不是在處理消息時(shí)阻塞,而是在等待消息通知時(shí)被阻塞。
比如select 函數(shù),假如傳入的最后一個timeout參數(shù)為NULL,那么如果所關(guān)注的事件沒有一個被觸發(fā),程序就會一直阻塞在這個select 調(diào)用處。
同步非阻塞形式實(shí)際上是效率低下的,想象一下你一邊打著電話一邊還需要抬頭看到底隊(duì)伍排到你了沒有,如果把打電話和觀察排隊(duì)的位置看成是程序的兩個操作的話,這個程序需要在這兩種不同的行為之間來回的切換,效率可想而知是低下的。
很多人會寫阻塞的read/write 操作,但是別忘了可以對fd設(shè)置O_NONBLOCK 標(biāo)志位,這樣就可以將同步操作變成非阻塞的了。
異步非阻塞形式效率更高,因?yàn)榇螂娫捠悄?等待者)的事情,而通知你則是柜臺(消息觸發(fā)機(jī)制)的事情,程序沒有在兩種不同的操作中來回切換。
比如說,這個人突然發(fā)覺自己煙癮犯了,需要出去抽根煙,于是他告訴大堂經(jīng)理說,排到我這個號碼的時(shí)候麻煩到外面通知我一下(注冊一個回調(diào)函數(shù)),那么他就沒有被阻塞在這個等待的操作上面,自然這個就是異步+非阻塞的方式了。
如果使用異步非阻塞的情況,比如aio_*組的操作,當(dāng)發(fā)起一個aio_read操作時(shí),函數(shù)會馬上返回不會被阻塞,當(dāng)所關(guān)注的事件被觸發(fā)時(shí)會調(diào)用之前注冊的回調(diào)函數(shù)進(jìn)行處理。
很多人會把同步和阻塞混淆,我想是因?yàn)楹芏鄷r(shí)候同步操作會以阻塞的形式表現(xiàn)出來,比如很多人會寫阻塞的read/write操作,但是別忘了可以對fd設(shè)置O_NONBLOCK標(biāo)志位,這樣就可以將同步操作變成非阻塞的了。但最根本是因?yàn)闆]有區(qū)分這兩個概念,比如阻塞的read/write操作中,其實(shí)是把消息通知機(jī)制和等待消息通知的狀態(tài)結(jié)合在了一起,在這里所關(guān)注的消息就是fd是否可讀/寫,而等待消息通知的狀態(tài)則是對fd可讀/寫等待過程中程序(線程)的狀態(tài)。當(dāng)我們將這個fd設(shè)置為非阻塞的時(shí)候,read/write操作就不會在等待消息通知這里阻塞,如果fd不可讀/寫則操作立即返回。
同樣的,很多人也會把異步和非阻塞混淆,因?yàn)楫惒讲僮饕话愣疾粫谡嬲腎O操作處被阻塞,比如如果用select函數(shù),當(dāng)select返回可讀時(shí)再去read一般都不會被阻塞,而是在select函數(shù)調(diào)用處阻塞。
4.總結(jié)阻塞與非阻塞
4.1.socket默認(rèn)阻塞
在默認(rèn)情況下是阻塞狀態(tài)的,這就使得發(fā)送以及接收操作處于阻塞的狀態(tài),即調(diào)用不會立即返回,而是進(jìn)入睡眠等待操作完成。
4.2 二者前提條件:產(chǎn)生系統(tǒng)調(diào)用
首先需要說明的是,不管阻塞還是非阻塞,在發(fā)送時(shí)都會將數(shù)據(jù)從應(yīng)用緩沖區(qū)拷貝到內(nèi)核緩沖區(qū)(SO_RCVBUF選項(xiàng)聲明,除非緩沖區(qū)大小為0)。?對于阻塞模式來說,系統(tǒng)資源沒有準(zhǔn)備就緒,就會被剝奪CPU。
從以下幾個方面去展開說明:
發(fā)送數(shù)據(jù)系列函數(shù)(send、write、writev、sendmsg)一次性copy到內(nèi)核的數(shù)據(jù)大于緩沖區(qū)當(dāng)前剩余可用的大小。
在阻塞模式下send操作將會等待所有數(shù)據(jù)均被拷貝到發(fā)送緩沖區(qū)后才會返回。如果當(dāng)前發(fā)送緩沖總大小為8192,已經(jīng)拷貝到緩沖的數(shù)據(jù)為8000,那剩余的大小為192,現(xiàn)在需要發(fā)送2000字節(jié)數(shù)據(jù),那阻塞發(fā)送就會等待緩沖區(qū)足夠把所有2000字節(jié)數(shù)據(jù)拷貝進(jìn)去,如第一次拷貝進(jìn)192字節(jié),當(dāng)緩沖區(qū)成功發(fā)送出1808字節(jié)后,再把應(yīng)用緩沖區(qū)剩余的1808字節(jié)拷貝到內(nèi)核緩沖,而后send操作返回成功發(fā)送字節(jié)數(shù)。
從上面的過程不難看出,阻塞的send操作返回的發(fā)送大小,必然是你參數(shù)中的發(fā)送長度的大小。
讀取數(shù)據(jù)系列函數(shù)(recv、read、readv、recvmsg)讀取的socket fd對應(yīng)的內(nèi)核buff還沒有就緒數(shù)據(jù)(還在等待對方輸入、還在等待網(wǎng)絡(luò)傳輸、亦或數(shù)據(jù)已經(jīng)到內(nèi)核了但數(shù)據(jù)有部分丟失還在等待“丟失重傳”等等)。
接收外來連接函數(shù)accept,沒有新的連接到達(dá),進(jìn)程會被剝奪CPU,進(jìn)入睡眠狀態(tài)。
發(fā)起外出連接函數(shù)conect,沒有和對端完成三次握手之前,此函數(shù)不會從系統(tǒng)調(diào)用返回,至少要阻塞數(shù)據(jù)包的一次往返時(shí)間(RTT)。
以上四類情況系統(tǒng)會剝奪當(dāng)前執(zhí)行者對CPU的持有權(quán)利,因?yàn)闆]有CPU,就無法執(zhí)行代碼指令,用戶看來就是程序“卡住/假死”了。
網(wǎng)絡(luò)編程經(jīng)典步驟:

?后續(xù)我將通過編程,提供多個demo去逐一講解以上函數(shù)的使用特性,只有更近一步的探索這些核心函數(shù),才能做到游刃有余,實(shí)現(xiàn)企業(yè)級千萬并發(fā)。
4.3非阻塞
對于非阻塞模式來說,系統(tǒng)資源沒有準(zhǔn)備就緒,但系統(tǒng)立即返回一個對應(yīng)錯誤標(biāo)示符,不會剝奪CPU,當(dāng)前執(zhí)行者可以(一般會間隔一段時(shí)間,如,調(diào)用sleep/usleep函數(shù))繼續(xù)做其他事情或者立即執(zhí)行下一次讀或者寫操作,如此反復(fù),直到完成。
