【GoLang 那點事】深入淺出那些你知道但不理解的併發模型

a_wei發表於2019-08-19

文章比較長,需要一些耐心才能看完

併發模型簡介

  • 併發:一個人同一時間應對多件事的能力
  • 並行:一個人同一時間處理多件事的能力(顯然一個人同一事件不能處理多件事,單核CPU不具備並行能力)
  • 可以理解為並行是併發的一種特殊情況
  • 併發模型的核心是為了提高提高CPU利用率,提高伺服器應對大量請求,海量資料處理的能力,單核CPU效能已經難以發展,各大廠商都在通過增加CPU個數來達到硬體處理能力的提高(摩爾定律),隨之而來在程式語言方面衍生出各個模型(其實就是處理問題的思路)用來壓榨硬體的效能,以使自己的系統併發能力得到提高。

    同步和非同步以及阻塞和非阻塞

  • 要了解各種併發模型思想,首先要了解什麼是同步,什麼是非同步?什麼是阻塞,什麼是非阻塞?
  • 舉一個例子來說明上面的概念,小明去買自己愛吃的燒雞
  • 同步阻塞的做法是小明付帳後一直盯著老闆製作燒雞,直到完成才高興的辦其它事了
  • 同步非阻塞的做法是小明付帳後不會一直盯著老闆,而是做其它事了,每隔一會來看看老闆做好了沒
  • 非同步阻塞的做法是小明付帳以後,不會盯著老闆做了,也不幹其它事,老闆做好了通知小明
  • 非同步非阻塞指的是小明付帳以後,幹自己的事去了,老闆做好了通知小明
  • 同步和非同步的本質是我輪詢你還是你回撥我
  • 阻塞和非阻塞的本質是當發生等待的時候我能不能幹其它的事

我們用IO操作再來來描述一下同步,非同步,阻塞,非阻塞的情況

  • 同步IO :執行緒發起read操作,呼叫作業系統,這是會有一次使用者態切換到核心態,核心開始等待資料到達,資料完成後,從核心拷貝到使用者態空間,這個過程執行緒是一直等待狀態的
  • 阻塞IO:執行緒發起read操作,然後執行緒一直處於等待狀態,直到IO操作完成,其實和上面的同步IO一樣
  • 非阻塞IO:執行緒發起read操作,使用者態切換到核心態,如果核心資料沒有準備好,立刻返回一個錯誤,執行緒根據錯誤決定每隔一會輪詢依次,當核心資料準備好後,會將資料從核心拷貝到使用者態空間這個時候執行緒是一隻處於等待狀態的,也就是說第一階段是非阻塞,第二階段還是阻塞的
  • 非同步IO:執行緒發起read操作後,便可以做其他事了,作業系統資料準備好後(已經拷貝到使用者態)會告訴執行緒。

程式和執行緒的區別

  • 程式是作業系統資源分配的基本單位
  • 執行緒是CPU排程的基本單位
  • 從作業系統層面去看是程式,從CPU層面去看是執行緒
  • 程式的空間是獨立,各個程式相互不干擾,每個程式擁有自己的程式記憶體,上下文環境,程式控制塊,一個程式至少有一個或者多個執行緒。執行緒屬於程式,執行緒要存在必須依賴於程式,執行緒共享程式的記憶體,但執行緒有自己的棧空間,能建立多少個執行緒也取決於程式記憶體的大小。
  • 執行緒的上下文切換代價比程式要小的多。
  • 程式之間強調的是通訊,執行緒之間強調的是同步(資料安全)
比較內容 程式 執行緒
CPU,記憶體 佔用cpu和記憶體更多 佔用記憶體少,cpu切換簡單
資料共享合和同步 資料共享比較複雜,需要通訊,同步簡單,應為資料是分開的 資料共享簡單,但同步比較複雜,需要鎖操作
建立和銷燬 程式是重量級的,建立和銷燬都比較複雜 執行緒是一種輕量級的程式,建立和銷燬簡單
程式設計和除錯 複雜 簡單
可靠想 程式之間相互獨立,不會影響,一個程式掛掉不會影響其它程式 一個執行緒掛掉可能導致整個程式都over
  • 上面列了一些簡單的比較,其實不同作業系統下有著一些較大差別,比如linux作業系統下,程式的建立和銷燬其實和執行緒建立和銷燬所需的代價差不多,具體需要在使用時深入調研。

從作業系統系統層面考量的併發模型

1、多程式單執行緒

  • 這種併發模型是應用程式啟動後主程式會預先建立一些子程式出來,每來一個請求都會由一個子程式處理請求,這種模型會比較穩定,程式之間不干擾,也不會產生執行緒安全問題,同時也可以引入一些第三方的非執行緒安全的模組進來,但記憶體消耗較大,建立程式對記憶體的消耗會比較大,並且cpu在多個程式間來回切換開銷也大,所以一般子程式不宜過多。典型的一些開源軟體如Apache伺服器在Apahce2.X之後新增了並行處理模組(MPM->Multi-Processing-Modules)Prefork就是這種併發模型

2、多程式多執行緒

  • 這種併發模型是在上面多程式的併發模型上演化而來,開啟多個子程式,每個子程式下面又會開啟多個執行緒,這種模式下併發承受壓力會比單純的多程式好許多,但在一些CPU密集型作業下未必會比多程式好,因為每一個程式下的多執行緒上下文不斷切換的開銷是非常大的,cpu本來就在多個程式間切換,現在又要在單個程式下的多個執行緒間切換,cpu大部分時間都在切換上下文了,真正用於計算的時間反而很少,因此影響了其效能,因此對於一些網頁請求或者偏IO類的操作這種模式會比多程式的好上一些,典型的一些開源軟體如Apache伺服器在Apahce2.X之後新增了並行處理模組(MPM->Multi-Processing-Modules)Worker就是這種併發模型

3、單程式多執行緒

  • 這種併發模型也是現在大多web後臺開發的一種模式,尤其在Java中,應用程式啟動後開啟主執行緒,之後的請求都通過執行緒池技術來支撐併發。作業系統能保證當執行緒數小於等於cpu的個數時,讓不同的執行緒執行在不同的cpu上,提高cpu的利用率,典型的如開源框架tomcat就是這種併發模型。

從編碼層面(各種框架)設計的併發模型

1、reactor模型

  • 傳統的基於多執行緒的client-server模式,客戶端每傳送一個請求,server就開啟一個執行緒處理客戶端請求,這種模式在併發量不是很大的情況下非常好,效能OK,編碼也簡單,但當併發量一旦突破上線,效能就會急劇下降,佔用更多記憶體,cpu頻繁的在多個執行緒間進行上下文切換,reactor模式是基於事件驅動的高併發模型,他把一次請求分成多個事件,比如(connect,read,write),每次事件發生的時候才去觸發對應的處理器處理,reactor架構的主要由以下幾個元件組成
    1. Handle(window中稱為控制程式碼,linux中稱為檔案描述符,比如一個網路socket,在這個Handle上可以發生很多事件,比如connect,read,write),
    2. SynchronousEventDemultiplexer(同步事件分離器,本質上是系統呼叫)
    3. EventHandler(事件處理介面),
    4. Concrete Event Handler(實現應用程式所特提供的特定事件處理介面),
    5. Reactor(反應器,迴圈執行事件,操作事件控制程式碼的增刪改查操作,分發事件)
  • 這種模式將請求和處理分離,有專門的accept執行緒監聽來自客戶端的請求,請求到來後,也有專門的執行緒池處理讀寫任務,同時也有對應的業務執行緒池處理具體的業務邏輯,reactor模式雖然效能這麼高,很多框架也在用,但reactor模式是同步的,主要體現在IO操作上會阻塞一直等待讀寫完成,如下圖

Golang

2、proactor模型

  • proactor也是基於事件驅動的一種併發模型,但protacor是非同步的,在IO操作時,proactor併發模型能夠和作業系統之間解耦,由作業系統核心完成讀寫操作之後主動傳送完成事件,這也是和reactor的最大區別,proactor由以下幾個元件組成:
    1. Handle(控制程式碼)
    2. AsynchronousOperationProcessor(非同步事件處理器)
    3. Asynchronous Operation(非同步操作)
    4. Completion Event Queue(完成事件佇列,非同步操作的結果放入佇列中)
    5. Proactor(主動器,提供完成事件的迴圈,進行事件分發處理後續邏輯)
    6. Completion Handler(完成事件介面)
    7. Concrete Completion Handler(完成事件業務邏輯,實現上面的事件介面)
  • 這種模式下真正實現IO的非同步操作,不發發生阻塞,其實觀察reactor和proactor併發模型,發現都是儘量減少執行緒在執行期間的阻塞,將原本在一條直線上完成的所有操作分割成多端,之間通過事件進行通訊,reactor註冊的是就緒事件,而proactor註冊的是完成事件,由一個統一中央事件分發器進行管理,協作,這兩種模型都依賴作業系統核心本身的支援,框架只是在作業系統本身的支援下呼叫作業系統的api實現了更高一層的封裝,proactor模型如下圖:

Golang

3、actor模型

  • 不管任何併發模型其實都離不開的資料之間的互動,都需要通訊,reactor,proactor這兩種模型都是通過共享記憶體來進行通訊,而actor強調的是通過通訊來共享記憶體,actor強調的是沒有共享,所有的執行緒之間都是訊息傳遞來實現通訊,資料互動,每一個actor就是一個執行緒,actor模型幾十年前就已經出現,但因為受制於當時硬體的發展並沒有被重視,隨著多核時代的到來,actor模型開始有了用武之地,其中golang的goroutine,channel就是actor模型的一種實現,actor模型更適合多核程式設計,分散式程式設計,actor模型通過訊息傳遞保證了內部資料的狀態只會由自己修改,所以內部資料的處理不會涉及到鎖,同步等問題,actor模型由以下幾個元件組成:

    1. state (狀態,狀態由actor自身內部維護)
    2. Behavior (行為,指的是actor中計算邏輯或者業務邏輯)
    3. MailBox(郵箱,郵箱是actor和actor之間通訊的橋樑)
  • actor模型主要解決的是併發程式設計帶來的鎖,同步等複雜性,事實上MailBox中也有鎖,同步的邏輯,試想一下,兩個actor通過MailBox進行通訊,一個寫,一個讀,就會有併發問題,actor模型也是做了更高層次的抽象,封裝,我們從程式設計角度或者架構角度來看actor是實現通過訊息傳遞來共享資料的模型設計,如下圖:

Golang

歡迎大家關注微信公眾號:“golang那點事”,更多精彩期待你的到來

Golang

參考文章

那小子阿偉

相關文章