Go 協程與併發機制(三)golang 中的協程
【背景】
在 golang 中,協程被認爲是輕量級的線程。
和線程不同的是,操作系統內核感知不到協程的存在,用戶態的協程依賴 golang 運行時自身提供的調度器進行管理。同時,golang 中的協程是從屬於某個線程的,協程和線程的對應關係爲 M:N,即多對多,golang 語言調度器可以將多個協程調度到一個線程中,一個協程也可能切換到多個線程執行。
那麼,爲什麼 golang 要在線程的基礎上抽象出協程的概念,用語言自己的調度器管理而不是直接操作線程呢?
01
golang 用協程的好處
1. 棧的大小
之前說過爲了避免棧溢出,默認棧會相對較大。而 golang 中協程棧默認爲 2KB,在實踐中經常會看到成千上萬的協程存在。因爲,線程的棧在運行時不能更改默認大小,而 golang 會在運行時動態監測棧的大小並動態的擴容。
2. 上下文切換的速度
協程的速度要快於線程。原因在於寫成切換不用經過操作系統用戶態與內核態的切換,並且 golang 中的協程切換隻需要保留很少的狀態和寄存器變量值。
這裏我們回顧下線程的上下文切換,當操作系統內核決定切換成不同的用戶線程時:
通過顯示系統調用(寫入某個文件或管道)或定時器中斷從用戶態切換到內核態,這裏需要保存用戶空間線程的寄存器並跳到內核代碼;
調度程序決定接下來運行哪個線程,並需要處理內核態的虛擬內存,以及新線程的頁表加載
內核恢復新線程的寄存器並將控制權交換用戶空間;
在這裏主要時間消耗痛點是每個線程都有自己的工作空間緩存,切換的時候會刷新緩存被新線程取代,兩個線程頻繁切換會導致很多這樣的抖動。
那如何改變上邊的問題:
兩個線程通過一個管道交互少量數據來避免用戶級線程和內核級線程的頻繁切換, go 進程中的衆多協程依託於操作系統調度線程到 CPU 執行,從而最終執行協程。
那 golang 運行時是怎麼調度管理協程和線程的關係呢?
02
golang GMP 模型前瞻
go 在併發編程中的優越性在於它們都是在語言運行時實現的,而不是委託給庫。Go 運行時實現了啓動它們並在它們之間切換,而無需遵循 OS 內核。消除了同步代碼和異步代碼之間的區別。
在慣用的 Go 程序中,你不會看到很多互斥鎖、條件變量和保護共享數據的關鍵區域。這是因爲 Go 鼓勵程序員改用通道,通道是語言內置的,具有 select 等很棒的功能。正確使用通道消除了對顯式鎖定的需要,更容易正確編寫、調整性能和調試。
Go 語言中經典的 GMP 概念模型生動地概括了線程與協程的關係:Go 協程中的衆多協程依託於線程,藉助操作系統將線程調度到 CPU 執行,從而最終執行協程。在 GMP 模型中, G 代表 Go 語言中的協程 (Goroutine);M 代表實際的線程, 而 P 代表的是 Go 邏輯處理器 (Process),Go 語言爲了方便協程調度於緩存,抽象出來邏輯處理器。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/Gqvk2fMGkqm-JneNtBaaiw