在學習Go語言記憶體管理部分過程中,發現了一個很有意思的問題,今天就藉助這篇文章:
- 1.把這個問題也拋給大家,建議大家看見這個問題後,可以先自己思考一番?之後再讀下文。
- 2.進一步強化大家對Go記憶體架構的理解
開始本篇文章之前,我們快速回顧下「Go記憶體架構」相關的核心知識點,溫故知新。
快速回顧「TCMalloc記憶體管理架構」
先來簡單回顧下「TCMalloc記憶體管理架構」。詳細講解可檢視之前的文章《18張圖解密新時代記憶體分配器TCMalloc》
痛點
多執行緒時代 --->
執行緒共享記憶體 --->
執行緒申請記憶體會產生競爭 --->
競爭加鎖 --->
加鎖影響效能。
解法
每個執行緒上增加記憶體快取。
簡易架構圖如下:
快速回顧「Go記憶體管理架構」
接著簡單回顧下「Go記憶體管理架構」。詳細講解可檢視之前的文章《淺析Go記憶體管理架構》
痛點
同上。
解法
同上,基於「TCMalloc」實現。
簡易架構圖如下:
有趣的問題
關於這個有趣的問題,細心的朋友可能已經發現了,不賣關子了問題如下:
為什麼Go的記憶體管理器的執行緒快取是mcache
被邏輯處理器p
持有,而並不是被真正的系統執行緒m
持有?
個人思考時間
是不是很有意思,關於這個問題。對面的你不妨先停下來思考幾分鐘:
為什麼?
解密
按照原TCMalloc
的設計思想,執行緒快取mcache
確實應該被繫結到系統執行緒M
上。
那麼我們就假設:按照原TCMalloc
的思想,把mcache
繫結系統執行緒M
上。接著我們只需要看看這個假設有什麼問題即可。
要論證這個假設需要先來簡單看看「Go的排程模型GMP
」。
Go的排程模型GMP
直接上入門級「Go的排程模型GMP
」架構圖:
關於「Go的排程模型GMP
」的原理,大家應該看了無數文章,我這裡就不細說了,如果還有不熟悉可以自行搜尋哈。
這裡簡單提下關於GMP
的入門級知識哈,其實GMP
對應的只是Go語言自身的邏輯結構而已,含義如下:
M
:代表結構體m
,全稱Machine
,這個結構體的核心是會和真正的系統執行緒thread
繫結。G
:代表結構體g
,全稱Goroutine
,這個結構體就是大家熟知的協程
,簡單理解其實就是這個結構體繫結了一個有著被併發執行需求的函式。P
:代表結構體p
,全稱Processor
,這個結構體表示邏輯處理器
,通過這個結構體和計算機的邏輯處理器建立對應關係,P
的數量通常和計算機的邏輯處理器數量一致通過runtime.GOMAXPROCS(runtime.NumCPU())
設定。
三者的簡單職責以及關係:
P
- 和一個
M
互相繫結 - 維護了一個可執行
G
的佇列
- 和一個
M
- 和一個
P
互相繫結 - 負責執行
G
的排程,通過排程當前M
繫結的P
的G
佇列、以及全域性G
佇列,達到G
可被併發執行的目的。 - 負責執行
P
排程過來的當前G
- 和一個
此階段結論:以上的排程過程P
的數量和M
的數量是一一對應的,所以把mcache
繫結系統執行緒M
上和P
看起來都可以。所以我們上面的假設「按照原TCMalloc
的思想,把mcache
繫結系統執行緒M
上」目前看起來確實也沒啥問題。
我們繼續往下看,一種特殊的場景M
會和P
解綁。
I/O操作的系統呼叫
當G
執行一個I/O操作的系統呼叫時,比如read
、write
,因為系統呼叫過程中的阻塞(原因:核心往使用者態拷貝資料的過程產生的阻塞,不在本文範疇,後續文章詳解)問題,會發生如下操作:
- 當前
G
(我們命名為g1
)的M
(我們命名為m1
)和當前的P
(我們命名為p1
)解綁 - 上面的
p1
會繫結一個其他的M
(m2
) m1
執行完成系統呼叫之後會被放到閒置M
連結串列裡
由於m1
會被放進閒置連結串列,這是不是就意味著m1
上的mcache
當前就不能被複用,所以這樣看起來是不是mcache
繫結到p1
上更合適。
結論: 由於M
可能因為執行一個I/O操作的系統呼叫被阻塞(原因:核心往使用者態拷貝資料的過程產生的阻塞),M
會和當前P
解綁,當前P
繫結其他閒置或者新的M
,之前的M
結束系統呼叫會被放進閒置M
連結串列。之前的M
的mcache
就不會得到有效的複用,反而mcache
繫結到P
上就不存在這個問題,所以mcache
繫結到P
上更合適。
檢視《Go語言輕鬆進階》系列更多內容
連結 http://tigerb.cn/go/#/kernal/