1. 程式人生 > 實用技巧 >通俗講解 非同步,非阻塞和 IO 複用

通俗講解 非同步,非阻塞和 IO 複用

通俗講解 非同步,非阻塞和 IO 複用

1. 閱前熱身
為了更加形象的說明同步非同步、阻塞非阻塞,我們以小明去買奶茶為例。

1.1 同步與非同步
同步與非同步的理解
同步與非同步的重點在訊息通知的方式上,也就是呼叫結果通知的方式。
同步: 當一個同步呼叫發出去後,呼叫者要一直等待呼叫結果的通知後,才能進行後續的執行。
非同步:當一個非同步呼叫發出去後,呼叫者不能立即得到呼叫結果的返回。
非同步呼叫,要想獲得結果,一般有兩種方式:

1.主動輪詢非同步呼叫的結果;
2.被呼叫方通過callback來通知呼叫方呼叫結果。

生活中的例子

同步買奶茶:小明點單交錢,然後等著拿奶茶;

非同步買奶茶:小明點單交錢,店員給小明一個小票,等小明奶茶做好了,再來取。

非同步買奶茶: 小明要想知道奶茶是否做好了,有兩種方式:

1.小明主動去問店員,一會就去問一下:“奶茶做好了嗎?”…直到奶茶做好。這叫輪訓。
2.等奶茶做好了,店員喊一聲:“小明,奶茶好了!”,然後小明去取奶茶。這叫回調。

1.2 阻塞與非阻塞

阻塞與非阻塞的理解

阻塞與非阻塞的重點在於進/執行緒等待訊息時候的行為,也就是在等待訊息的時候,當前進/執行緒是掛起狀態,還是非掛起狀態。

阻塞呼叫在發出去後,在訊息返回之前,當前進/執行緒會被掛起,直到有訊息返回,當前進/執行緒才會被啟用.

非阻塞呼叫在發出去後,不會阻塞當前進/執行緒,而會立即返回。

生活中的例子

阻塞買奶茶:小明點單交錢,乾等著拿奶茶,什麼事都不做;
非阻塞買奶茶:小明點單交錢,等著拿奶茶,等的過程中,時不時刷刷微博、朋友圈。

1.3 總結

通過上面的分析,我們可以得知:

1.同步與非同步,重點在於訊息通知的方式;
2.阻塞與非阻塞,重點在於等訊息時候的行為。

所以,就有了下面4種組合方式:

1.同步阻塞:小明在櫃檯乾等著拿奶茶;
2.同步非阻塞:小明在櫃檯邊刷微博邊等著拿奶茶;
3.非同步阻塞:小明拿著小票啥都不幹,一直等著店員通知他拿奶茶;
4.非同步非阻塞:小明拿著小票,刷著微博,等著店員通知他拿奶茶。

2. IO 複用

IO 複用例子說明

假設你是一個機場的空管,你需要管理到你機場的所有的航線, 包括進港,出港,有些航班需要放到停機坪等待,有些航班需要去登機口接乘客。

你會怎麼做?

最簡單的做法,就是你去招一大批空管員,然後每人盯一架飛機, 從進港,接客,排位,出港,航線監控,直至交接給下一個空港,全程監控。

那麼問題就來了:

很快你就發現空管塔裡面聚集起來一大票的空管員,交通稍微繁忙一點,新的空管員就已經擠不進來了。空管員之間需要協調,屋子裡面就1,2個人的時候還好,幾十號人以後 ,基本上就成菜市場了。

空管員經常需要更新一些公用的東西,比如起飛顯示屏,比如下一個小時後的出港排期,最後你會很驚奇的發現,每個人的時間最後都花在了搶這些資源上。

現實上我們的空管同時管幾十架飛機稀鬆平常的事情:

他們怎麼做的呢?這個東西叫flight progress strip。

每一個塊代表一個航班,不同的槽代表不同的狀態,然後一個空管員可以管理一組這樣的塊(一組航班),而他的工作,就是在航班資訊有新的更新的時候,把對應的塊放到不同的槽子裡面。

這個東西現在還沒有淘汰哦,只是變成電子的了而已。

是不是覺得一下子效率高了很多,一個空管塔裡可以排程的航線可以是前一種方法的幾倍到幾十倍。

如果你把每一個航線當成一個Sock(I/O 流),空管當成你的服務端Sock管理程式碼的話.

第一種方法就是最傳統的多程序併發模型 (每進來一個新的I/O流會分配一個新的程序管理。)
第二種方法就是I/O多路複用 (單個執行緒,通過記錄跟蹤每個I/O流(sock)的狀態,來同時管理多個I/O流 。)

其實I/O多路複用這個坑爹翻譯可能是這個概念在中文裡面如此難理解的原因。所謂的I/O多路複用在英文中其實叫 I/O multiplexing.

重要的事情再說一遍: I/O multiplexing 這裡面的 multiplexing 指的其實是在單個執行緒通過記錄跟蹤每一個Sock(I/O流)的狀態(對應空管塔裡面的Fight progress strip槽)來同時管理多個I/O流. 發明它的原因,是儘量多的提高伺服器的吞吐能力。

是不是聽起來好拗口,看個圖就懂了:

在同一個執行緒裡面, 通過撥開關的方式,來同時傳輸多個I/O流,

最初級的I/O複用

所謂的I/O複用,就是多個I/O可以複用一個程序。

採用非阻塞的模式,當一個連線過來時,我們不阻塞住,這樣一個程序可以同時處理多個連線了。

比如一個程序接受了10000個連線,這個程序每次從頭到尾的問一遍這10000個連線:“有I/O事件沒?有的話就交給我處理,沒有的話我一會再來問一遍。”
然後程序就一直從頭到尾問這10000個連線,如果這1000個連線都沒有I/O事件,就會造成CPU的空轉,並且效率也很低,不好不好。

升級版的I/O複用

上面雖然實現了基礎版的I/O複用,但是效率太低了。於是偉大的程式猿們日思夜想的去解決這個問題…終於!

我們能不能引入一個代理,這個代理可以同時觀察許多I/O流事件呢?

當沒有I/O事件的時候,這個程序處於阻塞狀態;當有I/O事件的時候,這個代理就去通知程序醒來?

於是,早期的程式猿們發明了兩個代理—select、poll。

select、poll代理的原理是這樣的:

當連線有I/O流事件產生的時候,就會去喚醒程序去處理。
但是程序並不知道是哪個連線產生的I/O流事件,於是程序就挨個去問:“請問是你有事要處理嗎?”……問了99999遍,哦,原來是第100000個程序有事要處理。那麼,前面這99999次就白問了,白

白浪費寶貴的CPU時間片了!痛哉,惜哉…
1.select是第一個實現 (1983 左右在BSD裡面實現)
2.1997年實現了poll.
3.select與poll原理是一樣的,只不過select只能觀察1024個連線,poll可以觀察無限個連線。

上面看了,select、poll因為不知道哪個連線有I/O流事件要處理,效能也挺不好的。

那麼,如果發明一個代理,每次能夠知道哪個連線有了I/O流事件,不就可以避免無意義的空轉了嗎?

於是,超級無敵、閃閃發光的epoll,於5年以後, 在2002年被大神 Davide Libenzi 發明出來了。

epoll IO多路複用

epoll代理的原理是這樣的:

當連線有I/O流事件產生的時候,epoll就會去告訴程序哪個連線有I/O流事件產生,然後程序就去處理這個程序。如此,多高效!

epoll 可以說是I/O 多路複用最新的一個實現,epoll 修復了poll 和select絕大部分問題, 比如:

1.epoll 現在是執行緒安全的。
2.epoll 現在不僅告訴你sock組裡面數據,還會告訴你具體哪個sock有資料,你不用自己去找了。

可是epoll 有個致命的缺點,只有linux支援。於是其他的平臺實現型別的多路複用,比如BSD上面對應的是kqueue, win下對應的iocp

epoll和select/poll區別

簡單說epoll和select/poll最大區別是

1.epoll內部使用了mmap共享了使用者和核心的部分空間,避免了資料的來回拷貝
2.epoll基於事件驅動,epoll_ctl註冊事件並註冊callback回撥函式,epoll_wait只返回發生的事件避免了像select和poll對事件的整個輪尋操作。

3. Nginx 非同步,非阻塞,IO多路複用

Nginx 這樣出眾,正是他採用了非同步,非阻塞,IO多路複用。

Nginx之前是單程序的。看下他的程序。1個master程序,2個work程序。

$ pstree |grep nginx
 |-+= 81666 root nginx: master process nginx
 | |--- 82500 nobody nginx: worker process
 | \--- 82501 nobody nginx: worker process

每進來一個request,會有一個worker程序去處理。但不是全程的處理,處理到什麼程度呢?處理到可能發生阻塞的地方,比如向上遊(後端)伺服器轉發request,並等待請求返回。那麼,這個處理的worker不會這麼傻等著,他會在傳送完請求後,註冊一個事件:“如果upstream返回了,告訴我一聲,我再接著幹”。於是他就休息去了。這就是非同步。此時,如果再有request 進來,他就可以很快再按這種方式處理。這就是非阻塞和IO多路複用。而一旦上游伺服器返回了,就會觸發這個事件,worker才會來接手,這個request才會接著往下走。這就是非同步回撥。

參考檔案:

https://segmentfault.com/a/1190000007614502
https://www.zhihu.com/question/32163005
https://www.zhihu.com/question/22062795