來自:程式設計新說
常規的誤區
假設有一個展示使用者詳情的需求,分兩步,先呼叫一個HTTP介面拿到詳情資料,然後使用適合的檢視展示詳情資料。
如果網速很慢,程式碼發起一個HTTP請求後,就卡住不動了,直到十幾秒後才拿到HTTP響應,然後繼續往下執行。
這個時候你問別人,剛剛程式碼發起的這個請求是不是一個同步請求,對方一定回答是。這是對的,它確實是。
但你要問它為什麼是呢?對方一定是這樣回答的,“因為發起請求後,程式碼就卡住不動了,直到拿到響應後才可以繼續往下執行”。
我相信很多人也都是這樣認為的,其實這是不對的,是把因果關係搞反了:
不是因為程式碼卡住不動了才叫同步請求,而是因為它是同步請求所以程式碼才卡住不動了。
至於為什麼能卡住不動,這是由作業系統和CPU決定的:
因為核心空間裡的對應函式會卡住不動,造成使用者空間發起的系統呼叫卡住不動,繼而使程式裡的使用者程式碼卡住不動了。
因此卡住不動了只是同步請求的一個副作用,並不能用它來定義同步請求,那該如何定義呢?
同步和非同步
所謂同步,指的是協同步調。既然叫協同,所以至少要有2個以上的事物存在。協同的結果就是:
多個事物不能同時進行,必須一個一個的來,上一個事物結束後,下一個事物才開始。
那當一個事物正在進行時,其它事物都在幹嘛呢?
嚴格來講這個並沒有要求,但一般都是處於一種“等待”的狀態,因為通常後面事物的正常進行都需要依賴前面事物的結果或前面事物正在使用的資源。
因此,可以認為,同步更希望關註的是從宏觀整體來看,多個事物是一種逐個逐個的序列化關係,絕對不會出現交叉的情況。
所以,自然也不太會去關註某個瞬間某個具體事物是處於一個什麼狀態。
把這個理論應用的出神入化的非“排隊”莫屬。凡是在資源少需求多的場景下都會用到排隊。
比如排隊買火車票這件事:
其實售票大廳更在意的是旅客一個一個的到視窗去買票,因為一次只能賣一張票。
即使大家一窩蜂的都圍上去,還是一次只能賣一張票,何必呢?擠在一起又不安全。
只是有些人素質太差,非要往上擠,售票大廳迫不得已,採用排隊這種形式來達到自己的目的,即一個一個的買票。
至於每個旅客排隊時的狀態,是看手機呀還是說話呀,根本不用去在意。
除了這種由於資源導致的同步外,還存在一種由於邏輯上的先後順序導致的同步。
比如,先更新程式碼,然後再編譯,接著再打包。這些操作由於後一步要使用上一步的結果,所以只能按照這種順序一個一個的執行。
關於同步還需知道兩個小的點:
一是範圍,並不需要在全域性範圍內都去同步,只需要在某些關鍵的點執行同步即可。
比如食堂只有一個賣飯視窗,肯定是同步的,一個人買完,下一個人再買。但吃飯的時候也是一個人吃完,下一個人才開始吃嗎?當然不是啦。
二是粒度,並不是隻有大粒度的事物才有同步,小粒度的事物也有同步。
只不過小粒度的事物同步通常是天然支援的,而大粒度的事物同步往往需要手工處理。
比如兩個執行緒的同步就需要手工處理,但一個執行緒裡的兩個陳述句天然就是同步的。
所謂非同步,就是步調各異。既然是各異,那就是都不相同。所以結果就是:
多個事物可以你進行你的、我進行我的,誰都不用管誰,所有的事物都在同時進行中。
一言以蔽之,同步就是多個事物不能同時開工,非同步就是多個事物可以同時開工。
註:一定要去體會“多個事物”,多個執行緒是多個事物,多個方法是多個事物,多個陳述句是多個事物,多個CPU指令是多個事物。等等等等。
阻塞和非阻塞
所謂阻塞,指的是阻礙堵塞。它的本意可以理解為由於遇到了障礙而造成的動彈不得。
所謂非阻塞,自然是和阻塞相對,可以理解為由於沒有遇到障礙而繼續暢通無阻。
對這兩個詞最好的詮釋就是,當今中國一大交通難題,堵車:
汽車可以正常通行時,就是非阻塞。一旦堵上了,全部趴窩,一動不動,就是阻塞。
因此阻塞關註的是不能動,非阻塞關註的是可以動。
不能動的結果就是隻能等待,可以動的結果就是繼續前行。
因此和阻塞搭配的詞一定是等待,和非阻塞搭配的詞一定是進行。
回到程式裡,阻塞同樣意味著停下來等待,非阻塞表明可以繼續向下執行。
阻塞和等待
等待只是阻塞的一個副作用而已,表明隨著時間的流逝,沒有任何有意義的事物發生或進行。
阻塞的真正含義是你關心的事物由於某些原因無法繼續進行,因此讓你等待。但沒必要乾等,你可以做一些其它無關的事物,因為這並不影響你對相關事物的等待。
在堵車時,你可以乾等。也可以玩手機、和別人聊天,或者打牌、甚至先去吃飯都行。因為這些事物並不影響你對堵車的等待。不過你的車必須獃在原地。
在計算機裡,是沒有人這麼靈活的,一般在阻塞時,選在乾等,因為這最容易實現,只需要掛起執行緒,讓出CPU即可。在條件滿足時,會重新排程該執行緒。
兩兩組合
所謂同步/非同步,關註的是能不能同時開工。
所謂阻塞/非阻塞,關註的是能不能動。
透過推理進行組合:
同步阻塞,不能同時開工,也不能動。只有一條小道,一次只能過一輛車,可悲的是還TMD的堵上了。
同步非阻塞,不能同時開工,但可以動。只有一條小道,一次只能過一輛車,幸運的是可以正常通行。
非同步阻塞,可以同時開工,但不可以動。有多條路,每條路都可以跑車,可氣的是全都TMD的堵上了。
非同步非阻塞,可以工時開工,也可以動。有多條路,每條路都可以跑車,很爽的是全都可以正常通行。
是不是很容易理解啊。其實它們的關註點是不同的,只要搞明白了這點,組合起來也不是事兒。
回到程式裡,把它們和執行緒關聯起來:
同步阻塞,相當於一個執行緒在等待。
同步非阻塞,相當於一個執行緒在正常執行。
非同步阻塞,相當於多個執行緒都在等待。
非同步非阻塞,相當於多個執行緒都在正常執行。
I/O
IO指的就是讀入/寫出資料的過程,和等待讀入/寫出資料的過程。一旦拿到資料後就變成了資料操作了,就不是IO了。
拿網路IO來說,等待的過程就是資料從網路到網絡卡再到核心空間。讀寫的過程就是核心空間和使用者空間的相互複製。
所以IO就包括兩個過程,一個是等待資料的過程,一個是讀寫(複製)資料的過程。而且還要明白,一定不能包括運算元據的過程。
阻塞IO和非阻塞IO
應用程式都是執行在使用者空間的,所以它們能操作的資料也都在使用者空間。按照這樣子來理解,只要資料沒有到達使用者空間,使用者執行緒就操作不了。
如果此時使用者執行緒已經參與,那它一定會被阻塞在IO上。這就是常說的阻塞IO。使用者執行緒被阻塞在等待資料上或複製資料上。
非阻塞IO就是使用者執行緒不參與以上兩個過程,即資料已經複製到使用者空間後,才去通知使用者執行緒,一上來就可以直接運算元據了。
使用者執行緒沒有因為IO的事情出現阻塞,這就是常說的非阻塞IO。
同步IO和同步阻塞IO
按照上文中對同步的理解,同步IO是指發起IO請求後,必須拿到IO的資料才可以繼續執行。
按照程式的表現形式又分為兩種:
在等待資料的過程中,和複製資料的過程中,執行緒都在阻塞,這就是同步阻塞IO。
在等待資料的過程中,執行緒採用死迴圈式輪詢,在複製資料的過程中,執行緒在阻塞,這其實還是同步阻塞IO。
網上很多文章把第二種歸為同步非阻塞IO,這肯定是錯誤的,它一定是阻塞IO,因為複製資料的過程,執行緒是阻塞的。
嚴格來講,在IO的概念上,同步和非阻塞是不可能搭配的,因為它們是一對相悖的概念。
同步IO意味著必須拿到IO的資料,才可以繼續執行。因為後續操作依賴IO資料,所以它必須是阻塞的。
非阻塞IO意味著發起IO請求後,可以繼續往下執行。說明後續執行不依賴於IO資料,所以它肯定不是同步的。
因此,在IO上,同步和非阻塞是互斥的,所以不存在同步非阻塞IO。但同步非阻塞是存在的,那不叫IO,叫運算元據了。
所以,同步IO一定是阻塞IO,同步IO也就是同步阻塞IO。
非同步IO和非同步阻塞/非阻塞IO
按照上文中對非同步的理解,非同步IO是指發起IO請求後,不用拿到IO的資料就可以繼續執行。
使用者執行緒的繼續執行,和作業系統準備IO資料的過程是同時進行的,因此才叫做非同步IO。
按照IO資料的兩個過程,又可以分為兩種:
在等待資料的過程中,使用者執行緒繼續執行,在複製資料的過程中,執行緒在阻塞,這就是非同步阻塞IO。
在等待資料的過程中,和複製資料的過程中,使用者執行緒都在繼續執行,這就是非同步非阻塞IO。
第一種情況是,使用者執行緒沒有參與資料等待的過程,所以它是非同步的。但使用者執行緒參與了資料複製的過程,所以它又是阻塞的。合起來就是非同步阻塞IO。
第二種情況是,使用者執行緒既沒有參與等待過程也沒有參與複製過程,所以它是非同步的。當它接到通知時,資料已經準備好了,它沒有因為IO資料而阻塞過,所以它又是非阻塞的。合起來就是非同步非阻塞IO。
PS:聰明的你或許發現了我沒有提多路復用IO,因為它值得專門撰文一篇。
朋友會在“發現-看一看”看到你“在看”的內容