1. Goroutine
1.1 進(jìn)程, 線程和協(xié)程和 goroutine
- 進(jìn)程,線程: 內(nèi)核態(tài)調(diào)度,搶占式 CPU 調(diào)度,
- 協(xié)程: 用戶態(tài)調(diào)度, 協(xié)作式 CPU 調(diào)度,需要協(xié)程主動(dòng)讓出 CPU 控制權(quán).
goroutine 本質(zhì)上就是協(xié)程, 不過 golang 對 goroutine 調(diào)度進(jìn)行了封裝和處理,當(dāng)遇到長時(shí)間或者進(jìn)行系統(tǒng)調(diào)用時(shí),會(huì)主動(dòng)讓出當(dāng)前 goroutine 的 CPU 控制權(quán),讓其他 goroutine 被調(diào)度.
總結(jié):
| goroutine | 線程 | |
|---|---|---|
| 棧大小 | 2KB | 8MB |
| 調(diào)度 | 用戶態(tài)調(diào)度 | 內(nèi)核態(tài)調(diào)度 |
| CPU | 協(xié)作式 | 搶占式 |
1.2 實(shí)現(xiàn)原理
一般情況網(wǎng)絡(luò)服務(wù)器會(huì)有大量的連接需要處理,且總是大量的 I/O + 少量的計(jì)算,一個(gè)連接建立一個(gè)線程會(huì)消耗大量系統(tǒng)資源(1. 每個(gè)線程棧大小為 8MB, 2.頻繁的 I/O 會(huì)阻塞當(dāng)前線程使別的線程被調(diào)度,線程切換會(huì)帶來大量的開銷),所以這樣不可行.而使用基于事件模式的異步編程模型(select, poll, epoll)可以使用少量的線程來服務(wù)大量的網(wǎng)絡(luò)連接和 I/O 操作,但是需要開發(fā)人員手動(dòng)管理每個(gè)連接的上下文,大幅度提高程序的復(fù)雜度.
goroutine是在應(yīng)用層實(shí)現(xiàn)的類似于線程的編程模型,程序員可以像編寫多線程代碼一樣編寫協(xié)程代碼,但協(xié)程運(yùn)行時(shí)卻和異步編程模型相同.
golang 對各種 I/O函數(shù)進(jìn)行了封裝,其內(nèi)部調(diào)用了操作系統(tǒng)的異步 I/O 函數(shù),當(dāng)這些異步函數(shù)返回 busy 或者 blocking 時(shí),golang 就將現(xiàn)有的執(zhí)行序列保存,讓程序去拉另外一個(gè)goroutine(就緒的)的代碼來執(zhí)行,當(dāng) I/O就緒時(shí)(基于epoll,select 等)繼續(xù)在重新調(diào)度剛才的goroutine.
1.3 goroutine 調(diào)度
- 協(xié)作式調(diào)度即非搶占式,程序的切換方式取決于程序本身
- 搶占式調(diào)度: 程序的切換由操作系統(tǒng)(processor)控制
Go1.14 之前 Golang 為協(xié)作式的,在一下情況會(huì)進(jìn)行 goroutine 切換
- 由于系統(tǒng)調(diào)用而等待
- 等待讀取或?qū)懭胛淳彌_的通道
- 由于 time.Sleep() 而等待
- mutex, select, waitGroup等
此外Go 會(huì)啟動(dòng)一個(gè) sysmon 協(xié)程,該函數(shù)實(shí)現(xiàn)了搶占式調(diào)度的功能.sysmon運(yùn)行在 M,切不需要 P.
當(dāng)sysmon發(fā)現(xiàn) M 已經(jīng)運(yùn)行一個(gè) G 10ms 以上時(shí), 他會(huì)將該 G 的內(nèi)部參數(shù)preempt設(shè)置為true,當(dāng)該 G 進(jìn)行函數(shù)調(diào)用時(shí), G 會(huì)檢查自己的 preempt 標(biāo)志,如果為true, 則它將自己與 M 分離并推入"全局隊(duì)列". 現(xiàn)在,搶占就成功完成.但是foo {}僅是一個(gè)死循環(huán),其并不會(huì)發(fā)生搶占.
1.3.1 異步搶占
在 Go1.14 中引入 非協(xié)作式搶占(異步搶占),是一種使用信號的簡單有效的算法.
首先, sysmon 仍然會(huì)檢測到運(yùn)行了 10 ms 以上的 G.然后 sysmon 向運(yùn)行 G 的 P 發(fā)送信號(SIGURG). Go 的信號處理程序會(huì)調(diào)用 P 上的 gsignal的 goroutine來處理該信號,將其映射到 M 而不是 G,并使其檢查該信號. gsignal 看到搶占信號,停止正在運(yùn)行的 G.使用GODEBUG=asyncpreemptoff=1 可以禁止異步搶占.
全局隊(duì)列是與“本地隊(duì)列”不同的隊(duì)列,本地隊(duì)列是存儲(chǔ) P 具有的 G。全局隊(duì)列有以下幾個(gè)作用。
存儲(chǔ)那些超過本地隊(duì)列容量(256)的 G
存儲(chǔ)由于各種原因而等待的 G
存儲(chǔ)由搶占標(biāo)志分離的 G
1.4 GPM
- G goroutine go 協(xié)程
- P processor 執(zhí)行器
- M machine 工作線程
1.4.1 M 工作線程
主線程 M0
運(yùn)行 runtime.main, 單個(gè)實(shí)例,
sysmon 線程
創(chuàng)建了新的 m, 這個(gè) m 不需要綁定 p 就可以執(zhí)行. 與整個(gè)調(diào)度系統(tǒng)脫離.
- checkdead, 檢查是否所有 goroutine 都已經(jīng) 鎖死, 如果鎖死直接調(diào)用 runtime.throw,強(qiáng)制退出. 這個(gè)操作只在啟動(dòng)的時(shí)候做一次
- 將 netpoll 返回的結(jié)果注入到全局 sched 的任務(wù)隊(duì)列
- 收回因?yàn)?syscall 而長時(shí)間阻塞的 p, 同時(shí)搶占哪些執(zhí)行時(shí)間過長的 g
- 如果 span 內(nèi)存閑置超過 5min,那么釋放掉
普通線程
就是 GPM 模型里的 M, M 對應(yīng)的就是操作系統(tǒng)的線程.
M 會(huì)從綁定的 p 的本地隊(duì)列、sched 中的全局隊(duì)列、netpoll 中獲取可運(yùn)行的 g,實(shí)在找不著還會(huì)去其它的 p 那里去偷。
2. map
golang 的 map 是 hashmap(c++ stl 是紅黑樹)
- 拉鏈法解決沖突,每個(gè) bucket 可以存儲(chǔ) 8 對 key/value
- 會(huì)預(yù)先分配一些 bucket 用于溢出
- 使用高位 hash 與運(yùn)算來選擇桶
- 使用漸進(jìn)式 hash 擴(kuò)容hash 表
- count/(2B) > 6.5 即翻倍擴(kuò)容
- 當(dāng) overflow >= 2^B 是觸發(fā)等量擴(kuò)容
3. 內(nèi)存分配
3.1 內(nèi)存分配核心思想
- 每次從操作系統(tǒng)申請一大塊內(nèi)存(并不實(shí)際分配)
- 采用 TMalloc 算法, 將內(nèi)存對象切分為多級,以降低鎖粒度
- 回收對象內(nèi)存時(shí),并沒有將其真正釋放掉,只是放回預(yù)先分配的大塊內(nèi)存中,以便復(fù)用.
3.2 內(nèi)存結(jié)構(gòu)
| spans | bitmap | arena |
|---|---|---|
| 512MB | 16GB | 512GB |
| 存放spans指針,每個(gè) spans 指向page | 保存 arena 對應(yīng)的某個(gè)地址是否存在對象,以及對象是否被 gc 掃描過 | 有 8kb 的 page 組成 |
3.2.1 arena
arena 即我們通常意義上的heap, 由連續(xù)的 page 組成, 多個(gè) page 組成spans,spans 用于具體分配內(nèi)存,對象分配時(shí)會(huì)選擇大小相近的 span,即一種大小的 span 只給一種確定大小的對象分配,這樣來減少內(nèi)存碎片的產(chǎn)生.
3.2.2 bitmap
bitmap 即位圖,一個(gè) byte 標(biāo)記 4 個(gè)對象,分別標(biāo)記對應(yīng)地址是否有存在對象和此對象是否被 gc 標(biāo)記過.
3.2.3
參考資料
https://xargin.com/go-scheduler/
https://blog.csdn.net/lsj1342/article/details/119797966