寫了一年 golang,來聊聊進程、線程與協程
進程
在早期的單任務計算機中,用戶一次只能提交一個作業,獨享系統的全部資源,同時也只能幹一件事情。進行計算時不能進行 IO 讀寫,但 CPU 與 IO 的速度存在巨大差異,一個作業在 CPU 上所花費的時間非常少,大部分時間在等待 IO。
爲了更合理的利用 CPU 資源,把內存劃分爲多塊,不同程序使用各自的內存空間互不干擾,這裏單獨的程序就是一個進程,CPU 可以在多個進程之間切換執行,讓 CPU 的利用率變高。
爲了實現 CPU 在多個進程之間切換,需要保存進程的上下文(如程序計數器、棧、內核數據結構等等),以便下次切換回來可以恢復執行。還需要一種調度算法,Linux 中採用了基於時間片和優先級的完全公平調度算法。
線程
多進程的出現是爲了解決 CPU 利用率的問題,那爲什麼還需要線程?答案是爲了減少上下文切換時的開銷
。
進程在如下兩個時間點可能會讓出 CPU,進行 CPU 切換:
-
進程阻塞,如網絡阻塞、代碼層面的阻塞(如鎖)、系統調用等
-
進程時間片用完,讓出 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 中,等待被調度
- 當 M0 中正在運行的 G0 阻塞時(如執行了一個系統調用),此時 M0 會休眠,它將放棄掛載的 P0,以便被其他 M 調度到
-
當 M0 系統調用結束後,會嘗試 “偷” 一個 P,如果不成功,M0 將 G0 放到全局的 runqueue 中
-
P 會定期檢查全局 runqueue,保證自己消化完 G 後有事可做,同時也會從其他 P 裏 “偷” G
從上述看來,MPG 模型似乎只限制了同時運行的線程數,但上下文切換隻發生在可運行的線程上,應該是有一定的作用,當然這只是一部分。
golang 在 runtime 層面攔截了可能導致線程阻塞的情況,並針對性優化,他們可分爲兩類:
-
網絡 IO、channel 操作、鎖:只阻塞 G,M、P 可用,即線程不會讓出時間片
-
系統調用:阻塞 M,P 需要切換,線程會讓出時間片
所以綜合來看,goroutine 會比線程切換開銷少。
總結
從單進程到多進程提高了 CPU 利用率;從進程到線程,降低了上下文切換的開銷;從線程到協程,進一步降低了上下文切換的開銷,使得高併發的服務可以使用簡單的代碼寫出來,技術的每一步發展都是爲了解決實際問題。
搜索關注微信公衆號 "捉蟲大師",後端技術分享,架構設計、性能優化、源碼閱讀、問題排查、踩坑實踐。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/g99CRajmqQj9QmHcW5GD5g