轉載過來主要是由於本身看的。原文:http://ring0.me/2014/11/sync-async-blocked/node
好多人搞不清這兩組概念之間的區別。咱們拿小明下載文件打個比方。nginx
- 同步阻塞:小明一直盯着下載進度條,到 100% 的時候就完成。
- 同步非阻塞:小明提交下載任務後就去幹別的,每過一段時間就去瞄一眼進度條,看到 100% 就完成。
- 異步阻塞:小明換了個有下載完成通知功能的軟件,下載完成就「叮」一聲。不太小明仍然一直等待「叮」的聲音(看起來很傻,不是嗎)
- 異步非阻塞:仍然是那個會「叮」一聲的下載軟件,小明提交下載任務後就去幹別的,聽到「叮」的一聲就知道完成了。
也就是說,同步/異步是下載軟件的通知方式,或者說 API 被調用者的通知方式。阻塞/非阻塞則是小明的等待方式,或者說 API 調用者的等待方式。git
在不一樣的場景下,同步/異步、阻塞/非阻塞的四種組合都有應用。github
同步阻塞
同步阻塞是最簡單的方式,就像咱們在 C 語言裏調用一個函數並等待其返回。數據庫
如 stat 系統調用獲取文件元數據,只有同步阻塞一種模式。我在訪問量很大的一個文件服務器(mirrors.ustc.edu.cn)上遇到過大量 nginx 進程處於 D(uninterruptible)狀態的問題,就是由於 stat 系統調用不提供非阻塞 I/O(O_NONBLOCK)選項(nginx 在能用非阻塞 I/O 的地方都用了非阻塞)。文件的元數據被從磁盤中讀入進來的時間裏,這個 nginx worker 進程只能在內核態苦苦等待而沒法作其餘事。不提供 O_NONBLOCK 選項,對內核開發者來講這是省事了,但對用戶來講就要付出性能的代價了。編程
同步非阻塞
同步非阻塞就是 「每隔一下子瞄一眼進度條」 的輪詢(polling)方式。緩存
同步非阻塞方式相比同步阻塞方式:服務器
- 優勢是可以在等待任務完成的時間裏幹其餘活了(包括提交其餘任務,也就是 「後臺」 能夠有多個任務在同時執行)。
- 缺點是任務完成的響應延遲增大了,由於每過一段時間纔去輪詢一次,而任務可能在兩次輪詢之間的任意時間完成。
因爲同步非阻塞方式須要不斷輪詢,而 「後臺」 可能有多個任務在同時進行,人們就想到了循環查詢多個任務的完成狀態,只要有任何一個任務完成,就去處理它。這就是所謂的 「I/O 多路複用」。UNIX/Linux 下的 select、poll、epoll 就是幹這個的(epoll 比 poll、select 效率高,作的事情是同樣的)。Windows 下則有 WaitForMultipleObjects 和 IO Completion Ports API 與之對應(Windows API 的命名簡直甩 POSIX API 幾條街有木有!)多線程
高併發的程序通常使用同步非阻塞方式而非多線程 + 同步阻塞方式。要理解這一點,首先要扯到併發和並行的區別。好比去某部門辦事須要依次去幾個窗口,辦事大廳裏的人數就是併發數,而窗口個數就是並行度。也 就是說併發數是指同時進行的任務數(如同時服務的 HTTP 請求),而並行數是能夠同時工做的物理資源數量(如 CPU 核數)。經過合理調度任務的不一樣階段,併發數能夠遠遠大於並行度,這就是區區幾個 CPU 能夠支持上萬個用戶併發請求的奧祕。在這種高併發的狀況下,爲每一個任務(用戶請求)建立一個進程或線程的開銷很是大。而同步非阻塞方式能夠把多個 I/O 請求丟到後臺去,這就能夠在一個進程裏服務大量的併發 I/O 請求。
異步非阻塞
異步非阻塞,就是把一件事丟到 「後臺」 去作,完成以後再通知。
在 Linux 中,通知的方式是 「信號」。
- 若是這個進程正在用戶態忙着作別的事(例如在計算兩個矩陣的乘積),那就強行打斷之,調用事先註冊的信號處理函數,這個函數能夠決定什麼時候以及如何 處理這個異步任務。因爲信號處理函數是忽然闖進來的,所以跟中斷處理程序同樣,有不少事情是不能作的,所以保險起見,通常是把事件 「登記」 一下放進隊列,而後返回該進程原來在作的事。
- 若是這個進程正在內核態忙着作別的事,例如以同步阻塞方式讀寫磁盤,那就只好把這個通知掛起來了,等到內核態的事情忙完了,快要回到用戶態的時候,再觸發信號通知。
- 若是這個進程如今被掛起了,例如無事可作 sleep 了,那就把這個進程喚醒,下次有 CPU 空閒的時候,就會調度到這個進程,觸發信號通知。
異步 API 說來輕巧,作來難,這主要是對 API 的實現者而言的。Linux 的異步 I/O(AIO)支持是 2.6.22 才引入的,還有不少系統調用不支持異步 I/O。Linux 的異步 I/O 最初是爲數據庫設計的,所以經過異步 I/O 的讀寫操做不會被緩存或緩衝,這就沒法利用操做系統的緩存與緩衝機制。
Windows API 裏的異步 I/O API(被稱爲 Overlapped I/O)則優雅得多,能夠在 ReadFileEx、WriteFileEx 等 I/O API 上指定回調函數,當 I/O 操做完成時就會調用它。這至關於在 「信號」 的基礎上提供了一層封裝。除了指定回調函數,這些異步 I/O 請求還可使用 「傳統」 的同步阻塞方式(WaitForSingleObject)、多路複用的同步非阻塞方式(WaitForMultipleObjects)來等待。多個異 步 I/O 請求也能夠綁定到一個 I/O Completion Port 上一塊兒等待。
不少人把 Linux 的 O_NONBLOCK 認爲是異步方式,但事實上這是前面講的同步非阻塞方式。因爲 Linux 的異步 I/O 難用,nginx 早期版本一直使用的是 O_NONBLOCK 和 epoll,從 0.8.11 開始支持異步 I/O,但默認使用的仍然是同步非阻塞方式。須要指出的是,雖然 Linux 上的 I/O API 略顯粗糙,但每種編程框架都有封裝好的異步 I/O 實現。操做系統少作事,把更多的自由留給用戶,正是 UNIX 的設計哲學,也是 Linux 上編程框架百花齊放的一個緣由。
異步阻塞
都有下載完成通知了,我還傻傻地盯着進度條幹什麼?這種看起來很傻的方式也是有用的。有時咱們的 API 只提供異步通知方式,例如在 node.js 裏,但業務邏輯須要的是作完一件過後作另外一件事,例如數據庫鏈接初始化後才能開始接受用戶的 HTTP 請求。這樣的業務邏輯就須要調用者是以阻塞方式來工做。
爲了在異步環境裏模擬 「順序執行」 的效果,就須要把同步代碼轉換成異步形式,這稱爲 CPS(Continuation Passing Style)變換。BYVoid 大神的 continuation.js 庫就是一個 CPS 變換的工具。用戶只需用比較符合人類常理的同步方式書寫代碼,CPS 變換器會把它轉換成層層嵌套的異步回調形式。
另一種使用阻塞方式的理由是下降響應延遲。若是採用非阻塞方式,一個任務 A 被提交到後臺,就開始作另外一件事 B,但 B 還沒作完,A 就完成了,這時要想讓 A 的完成事件被儘快處理(好比 A 是個緊急事務),要麼丟棄作到一半的 B,要麼保存 B 的中間狀態並切換回 A,任務的切換是須要時間的(不論是從磁盤載入到內存,仍是從內存載入到高速緩存),這勢必下降 A 的響應速度。所以,對實時系統或者延遲敏感的事務,有時採用阻塞方式比非阻塞方式更好。
最後補充一句,同步/異步的概念在不一樣語境下是不一樣的,本文說的是 API 或者 I/O。在其餘語境裏多是別的意思,例如分佈式系統裏的同步表示是各節點按照時鐘節拍同步,而異步是收到消息後當即執行。