我們先看下go1.0源碼當時是c實現的go的調度:
~~~go
static void
schedule(G *gp)
{
...
schedlock();
if(gp != nil) {
...
switch(gp->status){
case Grunnable:
case Gdead:
// Shouldn't have been running!
runtime·throw("bad gp->status in sched");
case Grunning:
gp->status = Grunnable;
gput(gp);
break;
}
gp = nextgandunlock();
gp->readyonstop = 0;
gp->status = Grunning;
m->curg = gp;
gp->m = m;
...
runtime·gogo(&gp->sched, 0);
}
~~~
這里調度的作用:
* 調用`schedlock`方法來獲取全局鎖。
* 獲取全局鎖成功后,將當前 Goroutine 狀態從 Running(正在被調度) 狀態修改為 Runnable(可以被調度)狀態。
* 調用`gput`方法來保存當前 Goroutine 的運行狀態等信息,以便于后續的使用。
* 調用`nextgandunlock`方法來尋找下一個可運行 Goroutine,并且釋放全局鎖給其他調度使用。
* 獲取到下一個待運行的 Goroutine 后,將其運行狀態修改為 Running。
* 調用`runtime·gogo`方法,將剛剛所獲取到的下一個待執行的 Goroutine 運行起來,進入下一輪調度。
GM 模型的缺點:
Go1.0 的 GM 模型的 Goroutine 調度器限制了用 Go 編寫的并發程序的可擴展性,尤其是高吞吐量服務器和并行計算程序。
GM調度存在的問題:
* 存在單一的全局 mutex(Sched.Lock)和集中狀態管理: mutex 需要保護所有與 goroutine 相關的操作(創建、完成、重排等),導致鎖競爭嚴重。
* Goroutine 傳遞的問題:
goroutine(G)交接(G.nextg):工作者線程(M's)之間會經常交接可運行的 goroutine。 而且可能會導致延遲增加和額外的開銷。每個 M 必須能夠執行任何可運行的 G,特別是剛剛創建 G 的 M。
* 每個 M 都需要做內存緩存(M.mcache):
這樣會導致資源消耗過大(每個 mcache 可以吸納到 2M 的內存緩存和其他緩存),數據局部性差。
* 頻繁的線程阻塞/解阻塞:
在存在 syscalls 的情況下,線程經常被阻塞和解阻塞。這增加了很多額外的性能開銷。
為了解決 GM 模型的以上諸多問題,在`Go1.1`時,`Dmitry Vyukov`在 GM 模型的基礎上,新增了一個 P(Processor)組件。并且實現了 Work Stealing 算法來解決一些新產生的問題。
加了 P 之后會帶來什么改變呢?
* 每個 P 有自己的本地隊列,大幅度的減輕了對全局隊列的直接依賴,所帶來的效果就是鎖競爭的減少。而 GM 模型的性能開銷大頭就是鎖競爭。
* 每個 P 相對的平衡上,在 GMP 模型中也實現了`Work Stealing`算法,如果 P 的本地隊列為空,則會從全局隊列或其他 P 的本地隊列中竊取可運行的 G 來運行,減少空轉,提高了資源利用率。
為什么要有P呢?
一般來講,M 的數量都會多于 P。像在 Go 中,M 的數量默認是10000,P 的默認數量的 CPU 核數。另外由于 M 的屬性,也就是如果存在系統阻塞調用,阻塞了 M,又不夠用的情況下,M 會不斷增加。
M 不斷增加的話,如果本地隊列掛載在 M 上,那就意味著本地隊列也會隨之增加。這顯然是不合理的,因為本地隊列的管理會變得復雜,且 Work Stealing 性能會大幅度下降。
M 被系統調用阻塞后,我們是期望把他既有未執行的任務分配給其他繼續運行的,而不是一阻塞就導致全部停止。
因此使用 M 是不合理的,那么引入新的組件 P,把本地隊列關聯到 P 上,就能很好的解決這個問題。
- Golang基礎
- Go中new與make的區別
- Golang中除了加Mutex鎖以外還有哪些方式安全讀寫共享變量
- 無緩沖Chan的發送和接收是否同步
- Golang并發機制以及它所使用的CSP并發模型.
- Golang中常用的并發模型
- Go中對nil的Slice和空Slice的處理是一致的嗎
- 協程和線程和進程的區別
- Golang的內存模型中為什么小對象多了會造成GC壓力
- Go中數據競爭問題怎么解決
- 什么是channel,為什么它可以做到線程安全
- Golang垃圾回收算法
- GC的觸發條件
- Go的GPM如何調度
- 并發編程概念是什么
- Go語言的棧空間管理是怎么樣的
- Goroutine和Channel的作用分別是什么
- 怎么查看Goroutine的數量
- Go中的鎖有哪些
- 怎么限制Goroutine的數量
- Channel是同步的還是異步的
- Goroutine和線程的區別
- Go的Struct能不能比較
- Go的defer原理是什么
- Go的select可以用于什么
- Context包的用途是什么
- Go主協程如何等其余協程完再操作
- Go的Slice如何擴容
- Go中的map如何實現順序讀取
- Go中CAS是怎么回事
- Go中的逃逸分析是什么
- Go值接收者和指針接收者的區別
- Go的對象在內存中是怎樣分配的
- 棧的內存是怎么分配的
- 堆內存管理怎么分配的
- 在Go函數中為什么會發生內存泄露
- G0的作用
- Go中的鎖如何實現
- Go中的channel的實現
- 棧的內存是怎么分配的2
- 堆內存管理怎么分配的2
- Go中的map的實現
- Go中的http包的實現原理
- Goroutine發生了泄漏如何檢測
- Go函數返回局部變量的指針是否安全
- Go中兩個Nil可能不相等嗎
- Goroutine和KernelThread之間是什么關系
- 為何GPM調度要有P
- 如何在goroutine執行一半就退出協程
- Mysql基礎
- Mysql索引用的是什么算法
- Mysql事務的基本要素
- Mysql的存儲引擎
- Mysql事務隔離級別
- Mysql高可用方案有哪些
- Mysql中utf8和utf8mb4區別
- Mysql中樂觀鎖和悲觀鎖區別
- Mysql索引主要是哪些
- Mysql聯合索引最左匹配原則
- 聚簇索引和非聚簇索引區別
- 如何查詢一個字段是否命中了索引
- Mysql中查詢數據什么情況下不會命中索引
- Mysql中的MVCC是什么
- Mvcc和Redolog和Undolog以及Binlog有什么不同
- Mysql讀寫分離以及主從同步
- InnoDB的關鍵特性
- Mysql如何保證一致性和持久性
- 為什么選擇B+樹作為索引結構
- InnoDB的行鎖模式
- 哈希(hash)比樹(tree)更快,索引結構為什么要設計成樹型
- 為什么索引的key長度不能太長
- Mysql的數據如何恢復到任意時間點
- Mysql為什么加了索引可以加快查詢
- Explain命令有什么用
- Redis基礎
- Redis的數據結構及使用場景
- Redis持久化的幾種方式
- Redis的LRU具體實現
- 單線程的Redis為什么快
- Redis的數據過期策略
- 如何解決Redis緩存雪崩問題
- 如何解決Redis緩存穿透問題
- Redis并發競爭key如何解決
- Redis的主從模式和哨兵模式和集群模式區別
- Redis有序集合zset底層怎么實現的
- 跳表的查詢過程是怎么樣的,查詢和插入的時間復雜度
- 網絡協議基礎
- TCP和UDP有什么區別
- TCP中三次握手和四次揮手
- TCP的LISTEN狀態是什么
- 常見的HTTP狀態碼有哪些
- 301和302有什么區別
- 504和500有什么區別
- HTTPS和HTTP有什么區別
- Quic有什么優點相比Http2
- Grpc的優缺點
- Get和Post區別
- Unicode和ASCII以及Utf8的區別
- Cookie與Session異同
- Client如何實現長連接
- Http1和Http2和Grpc之間的區別是什么
- Tcp中的拆包和粘包是怎么回事
- TFO的原理是什么
- TIME_WAIT的作用
- 網絡的性能指標有哪些