無阻塞 編程模型

無阻塞 編程模型 涉及到   異步回調流, 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  是 良性 的 。

相關文章
相關標籤/搜索