回調和協程:利用同步思路處理異步響應的本質

編程領域的同步和異步

  1. 同步:指一個執行序1在執行某個請求的時候,若該請求需要一段時間才能返回信息,那麼這個執行序將會一直等待下去,直到收到返回信息才繼續執行下去;
  2. 異步:指執行序不需要一直等下去,而是繼續執行下面的操作,不管其他執行序的狀態。當有消息返回時系統會通知指定執行序進行處理。這樣在等待操作完成的過渡事件,系統可以有效利用cpu的資源。

從以上定義可以看出同步和異步之間的區別在於主動權的所屬,基本上可以理解爲:

  • 同步操作主動權在發起者,操作者需要檢索指定條件是否滿足,如果滿足則繼續執行,否則繼續等待。
  • 異步操作主動權切換,操作者發起異步操作之後,主動權歸還系統,當系統滿足條件之後,系統會繼續調用接下來的處理步驟。

從本質上來看,同步的思維更加接近人的慣性思維,更加容易編寫成熟穩定的代碼,異步代碼更符合CPU硬件運行,更加容易編寫高效運行代碼。

兩者看似是一對相互對立的概念,然而在現代編程領域,除了裸寫嵌入式程序,一般情況下,同步和異步操作的區別不再明顯,更何況有不少編程手段立意用同步的編程方式編寫異步任務處理。

同步和異步是兩個相依相對的概念,從表現上來看,不同的範圍上一個任務可能表現出二象性。所以討論一個任務是否是同步任務是需要指定範圍的。

操作系統提供的抽象工具

作爲計算機所有資源的管理者,操作系統的強大無可置疑。爲了實現資源的高效管理,他提供了最基本的異步機制抽象,是最經典的異步處理的實現案例

爲了更加高效的利用計算機資源,操作系統基本上都具備進程和線程的概念。進程和線程封裝了程序運行需要的資源和代碼,也是基本的異步操作實體。

操作系統一般對進程和線程進行搶佔式調度,當執行序的時間片消耗完成,或者當執行序訪問了指定資源(例如磁盤I/O、Socket)之後,執行序(進程、線程)便會被剝奪CPU的所有權,讓渡給其他等待的執行序。

等到條件滿足,資源準備完成。系統會通知等待此資源的執行序,回覆該執行序的運行。

這就是一個基本異步機制封裝,整個處理過程正是異步操作的基本思路,發送異步請求-讓渡主動權-系統回調處理過程。然而這整個程序的編寫卻是同步代碼。從程序自身的角度上來看,代碼是同步運行的,自己等待條件滿足的過程中“The world was stoped”。

這是同步機制和異步機制的統一。所以不必特意的追求異步編程,我們同樣能夠享受異步操作的好處

同步代碼效率低的原因

從操作系統角度上來看,所謂原生異步代碼和同步代碼本質上沒有什麼不同,那爲什麼出現同步代碼比專門編寫的採用異步API的代碼效率低下的現象?

我覺得問題出現在同步代碼採用的API顆粒度更小、異步操作將阻塞操作隔離在用戶代碼空間

從讀取文件的角度上來看,基本上同步API以字節爲單位,而異步API絕大多數都是以數據塊爲單位,而且基本上都帶有大量緩存,系統調用的次數越少,鎖的使用也就越少,效率自然越高。

對於讀取文件這類獨佔資源的操作,代碼中調用同步API會立刻陷入等待,將主動權交給系統的線程調度。在操作系統中同時存在數百上千的線程,系統一視同仁,軟件所佔的CPU時間比例也就很小
而異步API會將請求存入指令隊列,系統如果存在其他執行序會立刻切換,直到線程時間片使用完畢,或者系統中不存在可以切換的非阻塞執行序。異步過程中只發生了指令存入隊列過程,並不存在阻塞操作,可以將CPU分到的時間片消耗乾淨,軟件以函數回調的方式切換執行序,因此只需要付出函數壓棧的成本
異步操作環境的後臺守護線程是真正執行阻塞操作的線程,他從隊列中獲取命令、執行阻塞API操作,等待結果完成,將完成結果存入結果隊列。

利用同步思路處理異步響應的原理

現代編程領域追求的是編寫效率和運行效率的統一,爲了運行效率的最大化,就必須採用異步編程方式,採用更有效率的API接口。

採用異步機制的編程方式基本手段是過程回調。回調函數一多起來,就讓人摸不到頭腦。在異步操作環境中,擁有運行時級別的緩衝隊列,因此能夠提供異步操作API,而異步操作API的使用如果採用函數回調的方式會大大增加思維成本。

因此需要將異步操作與同步操作統合起來,操作系統進程和線程在這方面爲我們提供了一個實例。也就是再增加一層緩存、融合回調提供路由機制,自行切換執行序2

可以看出正常的實現同步思路處理異步響應需要三層緩存和抽象。

  1. 操作系統抽象硬件的異步執行,提供邏輯上的同步接口、存在操作系統級別的緩存隊列。
  2. 異步環境封裝同步接口,提供異步操作接口,提供路由機制自由調用回調過程。存在運行時級別的緩存隊列。
  3. 用戶路由機制封裝異步操作API,提供同步邏輯編程體驗,存在用戶級別的路由層。

從架構級別上來看代碼層數量越多,效率越低,異步環境的存在使得API執行期間必須添加上隊列等待時間,因而效率偏低。
而從整體表現上來看,由於耗時操作都被轉移到守護線程,因此執行線程可以將時間片消耗完全。在CPU時間消耗的整體佔比更大,整體時間更長,運行任務更多。
至於切換過程,由於在實際代碼中消耗時間的主要是工作代碼,切換的過程佔比較小,對CPU執行效率影響不大。但是如果每個回調函數非常短小,消耗時間很短,頻繁的切換不同的執行序就會造成路由層消耗劇增。嚴重影響運行效率。

協程的實現

協程也就是所謂的用戶態線程,本質上是對於函數回調的封裝。

在協程的實現機制中基本上都包含一個狀態機。當用戶代碼發起異步請求的時候,在請求的同時或之前需要註冊指定狀態的回調過程,當異步請求完成後,狀態機會根據狀態回調指定的處理過程。

本質上協程並不是一個如同線程和進程的獨立運行的實體,他只是邏輯上的被人爲組合起來的執行序。由於並不能獨立執行,因此他被依附在操作系統的線程上。

由於協程的實現基本上是基於回調函數的語法糖,因此將回調函數與操作系統的進程和線程進行對比是十分不公平的,由於不是真正的物理執行序,將協程與進程和線程比較更加沒有道理

基本的協程實現採用一個線程執行所有的用戶代碼1:N,某些環境(go)採用了N:M的實現方式,允許多個執行序依附於不同操作系統線程。因此golang要求採用管道傳遞數值,本質上是一個數值阻塞隊列,需要採用這種緩衝機制避免線程衝突。

golang的實現較爲複雜,需要利用編譯器針對性的優化,因此專門發明了一種語言,而不是對於已有語言進行修改。


  1. 爲什麼採用執行序這個冷僻的概念而不是流行的進程或者線程的概念是因爲本文試圖從本質上解釋同步和異步的概念而不侷限於某個特定的環境。 ↩︎

  2. 經典的路由機制根據當前狀態自由調用回調函數,產生了多個執行序併發執行的現象,整個過程是發生在同一個操作系統線程中,基本上不存在並行執行。線程纔是操作系統執行的基本單位。 ↩︎

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章