本文已收錄 https://github.com/lkxiaolou/lkxiaolou 歡迎star,
行程
在早期的單任務計算機中,用戶一次只能提交一個作業,獨享系統的全部資源,同時也只能干一件事情,進行計算時不能進行 IO 讀寫,但 CPU 與 IO 的速度存在巨大差異,一個作業在 CPU 上所花費的時間非常少,大部分時間在等待 IO,
為了更合理的利用 CPU 資源,把記憶體劃分為多塊,不同程式使用各自的記憶體空間互不干擾,這里單獨的程式就是一個行程,CPU 可以在多個行程之間切換執行,讓 CPU 的利用率變高,
為了實作 CPU 在多個行程之間切換,需要保存行程的背景關系(如程式計數器、堆疊、內核資料結構等等),以便下次切換回來可以恢復執行,還需要一種調度演算法,Linux 中采用了基于時間片和優先級的完全公平調度演算法,
執行緒
多行程的出現是為了解決 CPU 利用率的問題,那為什么還需要執行緒?答案是為了減少背景關系切換時的開銷,
行程在如下兩個時間點可能會讓出 CPU,進行 CPU 切換:
- 行程阻塞,如網路阻塞、代碼層面的阻塞(鎖、sleep等)、系統呼叫等
- 行程時間片用完,讓出 CPU
而行程切換 CPU 時需要進行這兩步:
- 切換頁目錄以使用新的地址空間
- 切換內核堆疊和硬體背景關系
行程和執行緒在 Linux 中沒有本質區別,他們最大的不同就是行程有自己獨立的記憶體空間,而執行緒(同行程中)是共享記憶體空間,
在行程切換時需要轉換記憶體地址空間,而執行緒切換沒有這個動作,所以執行緒切換比行程切換代價更小,
為什么記憶體地址空間轉換這么慢?Linux 實作中,每個行程的地址空間都是虛擬的,虛擬地址空間轉換到物理地址空間需要查頁表,這個查詢是很慢的程序,因此會用一種叫做 TLB 的 cache 來加速,當行程切換后,TLB 也隨之失效了,所以會變慢,
綜上,執行緒是為了降低行程切換程序中的開銷,
協程
當我們的程式是 IO 密集型時(如 web 服務器、網關等),為了追求高吞吐,有兩種思路:
- 為每個請求開一個執行緒處理,為了降低執行緒的創建開銷,可以使用執行緒池技術,理論上執行緒池越大,則吞吐越高,但執行緒池越大,CPU 花在切換上的開銷也越大
執行緒的創建、銷毀都需要呼叫系統呼叫,每次請求都創建,高并發下開銷就顯得很大,而且執行緒占用記憶體是 MB 級別,數量不能太多
為什么執行緒越多 cpu 切換越多?準確來說是可執行的執行緒越多,cpu 切換越多,因為作業系統的調度要保證絕對公平,有可執行執行緒時,一定是要雨露均沾,所以切換次數變多
- 使用異步非阻塞的開發模型,用一個行程或執行緒接收請求,然后通過 IO 多路復用讓行程或執行緒不阻塞,省去背景關系切換的開銷
這兩個方案,優缺點都很明顯,方案1實作簡單,但性能不高;方案2性能非常好,但實作起來復雜,有沒有介于這兩者之間的方案?既要簡單,又要性能高,協程就解決了這個問題,
協程是用戶視角的一種抽象,作業系統并沒有這個概念,其主要思想是在用戶態實作調度演算法,用少量執行緒完成大量任務的調度,
協程需要解決執行緒遇到的幾個問題:
- 記憶體占用要小,且創建開銷要小
- 減少背景關系切換的開銷
第一點好實作,用戶態的協程,只是一個資料結構,無需系統呼叫,而且可以設計的很小,達到 KB 級別,
第二點只能減少背景關系切換次數來解決,因為協程的本質還是執行緒,其切換開銷在用戶態是無法降低的,只能通過降低切換次數來達到總體上開銷的減少,可以有如下手段:
- 讓可執行的執行緒盡量少,這樣切換次數必然會少
- 讓執行緒盡可能的處于運行狀態,而不是阻塞讓出時間片
Goroutine
goroutine 是 golang 實作的協程,其特點是在語言層面就支持,使用起來非常方便,它的核心是MPG調度模型:
- M:內核執行緒
- P:處理器,用來執行 goroutine,它維護了本地可運行佇列
- G:goroutine,代碼和資料結構
- S:調度器,維護M和P的資訊
除此之外還有一個全域可運行佇列,

- 在 golang 中使用 go 關鍵字啟動一個 goroutine,它將會被掛到 P 的 runqueue 中,等待被調度

2. 當 M0 中正在運行的 G0 阻塞時(如執行了一個系統呼叫),此時 M0 會休眠,它將放棄掛載的 P0,以便被其他 M 調度到

3. 當 M0 系統呼叫結束后,會嘗試“偷”一個 P,如果不成功,M0 將 G0 放到全域的 runqueue 中
- P 會定期檢查全域 runqueue,保證自己消化完 G 后有事可做,同時也會從其他 P 里“偷” G
從上述看來,MPG 模型似乎只限制了同時運行的執行緒數,但背景關系切換只發生在可運行的執行緒上,應該是有一定的作用,當然這只是一部分,
golang 在 runtime 層面攔截了可能導致執行緒阻塞的情況,并針對性優化,他們可分為兩類:
- 網路 IO、channel 操作、鎖:只阻塞 G,M、P 可用,即執行緒不會讓出時間片
- 系統呼叫:阻塞 M,P 需要切換,執行緒會讓出時間片
所以綜合來看,goroutine 會比執行緒切換開銷少,
總結
從單行程到多行程提高了 CPU 利用率;從行程到執行緒,降低了背景關系切換的開銷;從執行緒到協程,進一步降低了背景關系切換的開銷,使得高并發的服務可以使用簡單的代碼寫出來,技術的每一步發展都是為了解決實際問題,
搜索關注微信公眾號"捉蟲大師",后端技術分享,架構設計、性能優化、原始碼閱讀、問題排查、踩坑實踐,
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/300638.html
標籤:Go
上一篇:C語言 va_start 宏 - C語言零基礎入門教程
下一篇:將百度萬年歷存入自己的資料庫
