無阻塞 編程模型 涉及到 異步回調流, Task, async await, 線程池, 併發編程, 並行編程, 大併發架構, 操做系統 之上 編程模型 的 發展 等等 。html
我這段時間對 這個領域 的 現狀 進行了一些 收集整理 和 批判 , 請看 :程序員
《後線程時代 的 應用程序 架構》 http://www.javashuo.com/article/p-blioukym-ga.html數據庫
《我 支持 使用 async await》 http://www.javashuo.com/article/p-kfsmekfd-cv.html編程
單純 從 執行效率 看, 也許 同步方法 最直接, 效率也最高 。 只要 配合 線程池 合理使用 線程 就能夠 。服務器
異步方法 的 意義 在於 實現 無阻塞 模式, 閉包
而 無阻塞 模式 的 意義 要在 大併發 且 IO 等待時間顯著 、IO 可能長時間等待 、 IO 等待時間不肯定(可能有意外) 的時候 纔會 體現出來 。架構
什麼是 IO 等待 ? IO 等待 本質上是 CPU 對 外部設備 的 等待 。併發
從 應用 上說, IO 等待 就是 訪問數據庫, 調用 WebApi, 讀寫文件, RPC 等 。異步
假設 線程池 有 1000 個 線程, 能夠同時處理 1000 個 用戶 的 請求, 每一個請求 都 須要 訪問數據庫,async
若是 數據庫 的 查詢緩慢, 則 這 1000 個 線程 可能 都會 去等待 數據庫, 當有 第 1001 個 以上的 用戶 訪問 網站 時, 線程池 將 沒有 多餘 的 線程 去 處理 第 1001 個 以上的 用戶 的 請求, 這種狀況 若是 持續一段時間, 就會變成 服務器 不能提供 服務, 若是 數據庫 處於 「掛掉」 的 異常狀態, 則 Web 服務器 線程池 裏 的 1000 個 線程 都將 長期 等待數據庫 而 掛起, 這樣 服務器 就 不能提供 服務, 或者 變得 異常緩慢 (對 用戶而言) 。
微服務 的 「雪崩」, 大概 也是 從這裏來的 。
且 從 廣義 的 角度 來說, 線程池 的 1000 個 線程 原本 還能夠有一部分 去作 其它 工做(不須要 訪問數據庫 的 工做,或是 訪問 其它數據庫 的 工做), 但 都卡在 訪問 A 數據庫 這裏了 。
可是, 咱們 又不能 採用 無限制 的 建立線程(New Thread)的 方式, 過多的 線程 會 花費 比較多的 切換時間, 也會 佔用 比較大 的 內存空間, 好比 1 個線程 的 堆棧 是 1 MB, 則 1024 個 線程 的 堆棧空間 總和 就是 1024 * 1 MB = 1 GB 。
因此, 須要 對 線程池 裏的 線程 作一個 角色分工 來 解決 這個問題, 這就是 「m Work, n IO」 ,
「m Work, n IO」 就是 m 個 工做線程, n 個 IO 線程 。
m 個 工做線程 在 無阻塞 的 狀態下工做 。
若是是 單核 CPU, 則 能夠 退化爲 「1 Work, n IO」 。
若是 1 個 CPU 核 上 只有 1 個 工做線程, 則 稱爲 「單體」(monosome, monad) 。
Javascript 是 單體 。
咱們能夠 來 看看 3 種 方式 的 Sequence 圖 :
1 調用 同步方法, 如 fileStream.Read() 方法,
2 調用 async 方法 再 task.ContinueWith() ,
3 調用 async 方法, 使用 await,
1 調用 同步方法, 如 fileStream.Read() 方法,
2 調用 async 方法 再 task.ContinueWith() ,
3 調用 async 方法, 使用 await,
「狀態機」 就是 將 函數參數 、局部變量 等 上下文 保存在 「狀態」 中, 將 「狀態」 保存在 堆 裏, 以 取代 傳統的 函數調用 把 參數 、局部變量 等 上下文 保存在 棧 裏的 作法 。
假設 有個 Foo() 方法,
Foo()
{
…… // Part 1
await xxxAsync();
…… // Part 2
}
編譯器 會將 Foo() 方法 中 await 以前 的 代碼 變成一個 Foo_Part1() 方法, Foo() 方法 中 await 以後 的 代碼 變成一個 Foo_Part2() 方法,
這樣 Foo() 方法 就被 「分割」 成 3 個 部分 :
1 Foo_Part1()
2 await xxxAsync()
3 Foo_Part2()
在 執行 的 時候, 狀態機 就能夠 按 「步驟」 調用 這 3 個 部分,
先調用 Foo_Part1() , 再調用 xxxAsync(), 以後 轉入 異步方法 執行, 本次調用 結束 。
當 xxxAsync() 執行完成後, 會調用 回調, 回調 調用 狀態機, 狀態機 接着以前的 「步驟」, 繼續執行 Foo_Part2() 。
這整個 過程 連貫起來, 就是 Foo_Part1() -> xxxAsync() -> Foo_Part2, 這正還原了 程序員 寫的 源代碼 中的 執行流程 。
程序員 寫的 源代碼 看起來 是一個 順序 同步 的 執行過程, 但其實是一個 異步 無阻塞 的 執行過程 。
爲何要用 狀態機 ? 由於要實現 異步架構, 同時還要儘可能 保持 函數層層調用 的 邏輯層次結構 。
好比, 若是 在 執行中 拋出異常, 在 異常信息 中, 能夠看到 函數 的 調用層次, 能夠看到 異常 是從 「Foo_Part1()」 中 拋出來 的,
這樣 咱們 就 清楚 異常 出現 在 那一行代碼,
若是 異常 是 從 「Foo_Part2()」 中 拋出來 的, 那咱們也知道 異常 出如今 await xxxAsync(); 以後的 代碼 裏 。
因此, async await 是一個 語法糖, 有 網友 說是 編譯器 的 「黑魔法」, 我總以爲 async await 這個 語法糖 有點大, 能夠叫 「語法蛋糕」 。
而要實現 真正的 「n IO」 無阻塞, 還須要 操做系統 也用 無阻塞 的 方式 來 實現 IO 。
假設有 n 個 IO 線程, 操做系統 應該 用 1 個 或 n 個 線程 去 「輪流」 等待 多個設備 的 響應 或者 一個設備 對 多個請求 的 響應,
而不該該 固定 1 個 線程 去 等待 1 個 請求 的 響應 。
這種 用 線程 「輪流」 去 等待 設備 響應 的 作法, 就是 IOCP 。
理論上, 只要 CPU 的 處理速度 足夠快, 1 個 線程 能夠 等待(處理) n 個 設備 對 m 個 請求 的 響應 。
反之, 若是 固定 1 個線程 「負責」 等待 1 個 請求 的 響應, 則 n 個 請求 須要 n 個線程,
若是 某設備 的 處理速度 緩慢 或者 故障, 而 對 該設備 的 請求 是 頻繁 的, 則 IO 線程 都 會 去等待 這個 設備, 這就 堵塞 了 。
因而 就沒有 線程 來 處理 其它 設備 的 IO 了。
這就 回到了 本文 開篇 提出的問題 。
經過 上面 3 個 Sequence 圖, 咱們能夠看到 :
相比同步方法, 就 單次調用 而言, 異步方法 並不會 減小 線程切換 的 次數, 異步方法 的 意義 在於 無阻塞 。
可是 從 整體 來看, 無阻塞 顯著 的 減小了 線程 的 數量, 更少 的 線程 意味着 更少 的 切換 。
因此, 從 整體 來看, 異步方法 也是 減小了 線程 切換 次數 的 。
無阻塞 是 有利的, 是 計算機軟件體系 在 後線程時代 的 一次 發展進化 。
無阻塞 還能夠用於 SOA , 好比 SOA 中會有這樣的 場景, 一個業務 須要 調用 若干個 服務 來完成 。
這樣, 就能夠 這樣 寫代碼 :
Foo()
{
…… // 一些操做
Task t1 = Service1Async();
Task t2 = Service2Async();
Task t3 = Service3Async();
await Task.WhenAll( { t1, t2, t3} );
…… // 3 個 服務 都 調用 完成時 要 執行 的 操做
}
因爲 服務 完成的時間 多是 不肯定 的, 因此 若是 等 服務 1 完成 再 調用 服務 2, 服務 2 完成 再 調用 服務 3, 這樣 效率 就比較低 。
因此, 經過 無阻塞 的 方式, 併發調用 多個 服務, 而後 等待 服務 所有 完成, 再作下一步操做, 這樣 能夠 提升效率 。
固然, 這裏的 「等待」, 也是 無阻塞 的 。 ^^
在 無阻塞 編程 中, 不能 調用 Thread.Sleep() 來 延時, 這會 阻塞 線程, 佔用 線程,
而應該用 await Task.Delay() 方法 來 延時, 或是用 Timer 來設定一個 定時任務, 把 延時後 要作的 工做 放到這個 定時任務 裏,
固然, await Task.Delay() 更加的直觀, 但 我猜 await Task.Delay() 內部也是用 Timer 原理 實現的 。
而 用 Timer 定時任務 來實現 延時, 這和 Javascript 的 window.setTimeout() 又是 恰如其分 的 類似 。
簡單的狀況, Task t; t.ContinueWith( 回調 ); 能夠很好的完成 異步調用 。 Lambda 式 匿名函數 、閉包 以及 Task 的 封裝 已經 使 代碼 很 簡潔直觀 。
可是對於一些 場景, 好比 業務系統 三層架構 裏 DAL 層 訪問數據庫, 對數據進行一些處理後 返回 BL 層, BL 層 又把 結果 返回 UI 層,
咱們能夠調用 Async 方法 訪問數據庫, 以實現 無阻塞, 但這種須要對 結果 進行處理 並 層層返回 的 場景, 用 異步回調 的話 代碼 就很麻煩,
而 async await 正是 爲了 解決 「過多的 異步回調 把 代碼 切割的 支離破碎」 的 問題, 因此 async await 是 良性 的 。