一、Golang 的 slice、map、channel
1.1 slice vs array
a := make([]int, 100) //切片
b := [100]int{} //數(shù)組
array需指明長(zhǎng)度,長(zhǎng)度為常量且不可改變
array長(zhǎng)度為其類型中的組成部分(給參數(shù)為長(zhǎng)度100的數(shù)組的方法傳長(zhǎng)度為101的會(huì)報(bào)錯(cuò))
array在作為函數(shù)參數(shù)時(shí)會(huì)產(chǎn)生copy
golang所有函數(shù)參數(shù)都是值傳遞
array擴(kuò)容:cap<1024時(shí)乘2,否則乘1.25,預(yù)先分配內(nèi)存可以提升性能,直接使用index賦值而不是append可以提升性能
slice作為參數(shù)被修改時(shí),如果沒有發(fā)生擴(kuò)容,修改在原來(lái)的內(nèi)存中;如果發(fā)生了擴(kuò)容,修改會(huì)在新的內(nèi)存中。
使用[]Type{}或者make([]Type)初始化后,slice不為nil;使用var x[]Type后,slice為nil
1.2 Map:
map的值其實(shí)是指針,傳map傳的是指針,所以修改會(huì)影響整個(gè)map。
map的k、v都不可取地址,隨著map的擴(kuò)容地址會(huì)改變。map存的是值,會(huì)發(fā)生copy,因此不要在map里放很大的數(shù)組,很大的可以用指針來(lái)代替
map賦值會(huì)自動(dòng)擴(kuò)容,但刪除時(shí)不會(huì)自動(dòng)縮容。
map非線程安全,不能同時(shí)讀寫。
1.3 Channel
有鎖
緩沖channel和非緩沖的區(qū)別,緩沖會(huì)發(fā)生兩次copy,非緩沖發(fā)生一次
for+select closed channel會(huì)造成死循環(huán),select中的break無(wú)法跳出for循環(huán)
二、GOTT best practices
2.1 可讀性
- if else 和 happy path:有錯(cuò)誤應(yīng)該提前返回,盡量在正確返回時(shí),不加 indents(縮進(jìn))。
- init() 使用規(guī)范:在一些 package 中盡量不要使用 init(),定義一個(gè)可以被調(diào)用的 Initxxx() 函數(shù)顯示調(diào)用,防止運(yùn)行一些使用方不知道的代碼段。
- Comments:盡量寫函數(shù)做了什么,而不是怎么做的。
2.2 健壯性
- panic: 在 defer 中進(jìn)行 recover()
- Errors:使用 errors.Is() 和 errors.As() 來(lái)判斷 error 和斷言 error
相關(guān)文檔
2.3 效率
- 指針:函數(shù)修改參數(shù),應(yīng)該傳遞指針;參數(shù)中含有大量的內(nèi)容,避免拷貝可以傳遞指針;代碼風(fēng)格對(duì)齊,其他函數(shù)都是傳遞指針的;
Tricks:結(jié)構(gòu)體默認(rèn)傳遞指針;對(duì)于 for 中定義的變量在循環(huán)中會(huì)變,取其地址得到的值永遠(yuǎn)是最后一個(gè)。 - Deprecation:對(duì)于要廢棄的函數(shù),使用以下注釋格式,從而使得 golintci-lint 能夠檢測(cè)而出來(lái)。
// comments for the function
//
// Deprecated: use $funcName instead.
func funcToBeDeprecated(){
}
三、Golang 的強(qiáng)人鎖難
鎖的重要性:并發(fā)場(chǎng)景通過(guò) goroutine 和 channel 來(lái)實(shí)現(xiàn),但是 goroutine 之間可以共享內(nèi)存和變量,導(dǎo)致直接修改變量的時(shí)候,會(huì)存在沖突。使用鎖需要考慮的:性能、重入、公平
3.1 強(qiáng)人:最佳實(shí)踐
- 減少持有時(shí)間,縮小臨界區(qū)
可以的情況下盡量提前釋放,或者新定義一個(gè)函數(shù),函數(shù)內(nèi)部執(zhí)行臨界區(qū),以及上鎖釋放鎖,函數(shù)后進(jìn)行其他的邏輯操作 - 優(yōu)化鎖的粒度
空間換時(shí)間,分片操作,每個(gè)片加鎖。 - 讀寫分離
RWMutex;sync.Map(空間換時(shí)間) - 使用原子操作,避免使用鎖
atomic
3.2 鎖難:避免踩坑
- 不要拷貝Mutex
golang 函數(shù)傳參是復(fù)制拷貝,需要傳入指針 - 鎖不能重入
防止死鎖,一個(gè) goroutine 兩次調(diào)用 lock 會(huì)導(dǎo)致死鎖 - atomic.Value 誤用
存入的應(yīng)該是只讀對(duì)象,如果存入一個(gè) map,取出來(lái)對(duì)map操作,那么map還是存在并發(fā)讀寫問題 - 使用 race detector
go test\run\build\install -race xxx:加上 race 參數(shù),用來(lái)加強(qiáng)單測(cè)和壓測(cè)
3.3 暗黑:鎖的進(jìn)化
-
原子操作
- 古代:英特爾 80386 處理器,因?yàn)槭菃魏颂幚砥鳎灾恍枰iCPU,關(guān)閉中斷開關(guān),這樣操作就不會(huì)被中斷,操作完再打開中斷開關(guān)。低效:需要內(nèi)核態(tài)來(lái)操作中斷開關(guān)
- 近代:匯編代碼提供了 CMPXCHGL 指令,在該指令前加一個(gè) Lock 前綴,會(huì)鎖定內(nèi)存總線。低效:內(nèi)存總線稱為瓶頸
- 現(xiàn)代:MESI 緩存一致性協(xié)議(降低鎖的粒度:總線鎖->緩存行鎖)。緩存行的狀態(tài),由硬件同步。MESI 為 Modified, Exclusive, Shared, Invalid 縮寫。
- Invalid:無(wú)效。初始化狀態(tài),或者內(nèi)存不可用(被其他CPU修改,需要更新緩存)
- Exclusive:獨(dú)占。僅當(dāng)前CPU緩存了該內(nèi)存。
- Shared:共享。多個(gè)CPU緩存了該內(nèi)存。
- Modified:已修改、未寫回。需要其他CPU的緩存失效。某個(gè)CPU更新了緩存,但是還沒寫回內(nèi)存,其他CPU緩存的該內(nèi)存信息更新為 Invalid。

-
自旋鎖(Spin Lock)
- Linux 內(nèi)核中常見,適合等待時(shí)間比較小的場(chǎng)景
- Go 1.14 版本之前,沒有實(shí)現(xiàn)搶占式調(diào)度,必須某個(gè) goroutine 交出控制權(quán),因此自旋鎖會(huì)導(dǎo)致死鎖。如下圖:A等待B釋放鎖,但是執(zhí)行了GC,然后B被掛起,runtime需要等待A掛起,但是A在執(zhí)行自旋鎖,就發(fā)生了死鎖。需要在自旋鎖內(nèi)部調(diào)用一次 runtime.GoSched 來(lái)交出 CPU 控制權(quán)

-
Go's Mutex
- 效率優(yōu)先,兼顧公平。
-
Mutex 有自己的一個(gè)等待隊(duì)列,有自己的狀態(tài) state(正常模式和饑餓模式)。正常模式保證效率,饑餓模式保證公平。state是一個(gè)共用字段,由鎖標(biāo)志位,喚醒標(biāo)志位,饑餓標(biāo)志位和阻塞的goroutine個(gè)數(shù)組成。
Go's Mutex State 字段組成(mutexLocked mutexWoken mutexStarving 位為 1 分別表示鎖占用、鎖喚醒、饑餓模式、mutexWaiterShift 表示偏移量,默認(rèn)為3,state>>=mutexWaiterShift,state的值就表示當(dāng)前阻塞等待鎖的goroutine個(gè)數(shù)。最多可以阻塞2^29個(gè)goroutine) - 正常模式:goroutine等待隊(duì)列先進(jìn)先出;新來(lái)的goroutine先去搶占鎖,失敗了再進(jìn)入等待隊(duì)列;如果發(fā)現(xiàn)某個(gè)搶到鎖的 goroutine 等待時(shí)長(zhǎng) > 1ms,則切換到饑餓模式。
- 饑餓模式:嚴(yán)格排隊(duì),隊(duì)首接盤;犧牲效率,保證Pct99;適時(shí)回歸正常模式,保證效率。如果某個(gè)goroutine加鎖成功后,如果發(fā)現(xiàn)這個(gè)goroutine位于隊(duì)尾,或者等待時(shí)間小于1ms,那么就切換回正常模式。
- 提高效率的點(diǎn):1. 新來(lái)的先去搶鎖,減少了調(diào)度開銷。2. 充分利用緩存,提高執(zhí)行效率。







- Go's Once
type Once struct {
done uint32
m Mutex
}
func (o *Once) Do(f func()) {
if atomic.LoadUint32(&o.done) == 0 {
// Outlined slow-path to allow inlining of the fast-path.
o.doSlow(f)
}
}
func (o *Once) doSlow(f func()) {
o.m.Lock()
defer o.m.Unlock()
if o.done == 0 {
defer atomic.StoreUint32(&o.done, 1)
f()
}
}
-
源碼很簡(jiǎn)單,如上:
- 問題:1. 為什么 Do 里面用 atomic,doSlow 里面用 o.done==0?2. 為什么 doSlow 里面用 atomic 來(lái)設(shè)置 done?3. 為什么 doSlow 里面用 defer 設(shè)置值?可否直接設(shè)置?
- 解答:1. Do 里面沒有加鎖,如果直接 o.done == 0 可能觀測(cè)到非常規(guī)值,使用 atomic 保證操作有序。而 doSlow 里面已經(jīng)是鎖內(nèi)部,不可能存在其他的 goroutine 修改值,因此可以直接觀測(cè)。2. 由于可能存在其他 goroutine 在 Do 內(nèi)觀測(cè) done 的值,因此需要 atomic 設(shè)置值來(lái)保證有序性。3. 不可以直接設(shè)置,如果直接設(shè)置值再執(zhí)行f,那么可能 f 還沒執(zhí)行,別的 goroutine 已經(jīng)觀測(cè)到 done 為 1,直接 Do 中返回,但是由于 f 的初始化函數(shù)還沒完成,從而導(dǎo)致 panic(空指針等)。因此在 f 未執(zhí)行完的過(guò)程中,所有執(zhí)行 once.Do 的 goroutine 都被阻塞在 doSlow 的 Lock 階段,等待 f 執(zhí)行完成才可以返回。
- 異常:假設(shè) Do 使用 o.done == 0 來(lái)觀測(cè)值,讀取的同時(shí)當(dāng) atomic 正在修改值時(shí),讀取到的值可能是異常值;假設(shè)使用 o.done=1 來(lái)設(shè)定值,執(zhí)行的同時(shí)當(dāng)其他 goroutine 在 Do 中讀取 o.done 時(shí),可能看到異常值。
- 總結(jié):這里的兩個(gè) atomic 是為了保證多個(gè) goroutine 觀測(cè)和設(shè)定同時(shí)發(fā)生的有序性。而鎖操作的臨界區(qū)內(nèi)可以直接觀測(cè)變量值。
Go's WaitGroup
下文 第八部分。巧妙地避免了鎖的使用。鎖的進(jìn)化總結(jié)
單核:關(guān)中斷->CAS指令
多核:LOCK內(nèi)存總線->MESI協(xié)議
自旋鎖:效率和公平不夠好
Go Mutex:效率優(yōu)先,兼顧公平。思考探索

-
延伸閱讀
- 踩坑記:Go服務(wù)靈異panic:https://mp.weixin.qq.com/s/wmdmYDenmOY2un6ymlO6SA
- Go: 關(guān)于鎖的1234: https://mp.weixin.qq.com/s/TRE8_0wLYv22NHXpMmvKqw
- Go中鎖的那些姿勢(shì),估計(jì)你不知道: https://studygolang.com/articles/26030
- Race Detector: https://blog.golang.org/race-detector
- sync: mutex.TryLock:https://github.com/golang/go/issues/6123
- Recursive locking in Go:https://stackoverflow.com/questions/14670979/recursive-locking-in-go
四、Golang 并發(fā)數(shù)據(jù)結(jié)構(gòu)和算法實(shí)踐
引言
scalable:當(dāng)計(jì)算資源更多時(shí),性能會(huì)有提升

4.1 并發(fā)安全問題
- Data Race
原因:多個(gè) goroutine 同時(shí)接觸一個(gè)變量,行為不可預(yù)知。
認(rèn)定條件:兩個(gè)及以上 goroutine
一寫多讀:atomic
多寫一讀:Lock + atomic
多寫多讀:Lock + atomic
4.2 實(shí)踐一:有序鏈表并行化
定義插入刪除的多個(gè)步驟,舉例不同場(chǎng)景,考慮并發(fā)情況下是否滿足,如何調(diào)整步驟。
4.3 實(shí)踐二:skiplist并行化
從 4.2 延伸過(guò)來(lái),每一層的 list 都可以使用 4.2 的實(shí)現(xiàn)。
4.4 總結(jié)
五、Golang 并發(fā)數(shù)據(jù)結(jié)構(gòu)和算法實(shí)踐
5.1 調(diào)度循環(huán)的建立
- GM 模型和 GMP 模型
- 調(diào)度循環(huán)的建立

5.2 協(xié)作與搶占
- 調(diào)度器的坑
go 運(yùn)行一個(gè)死循環(huán)在 go1.13 版本會(huì)被卡死,go1.14 引入基于信號(hào)的搶占,從而不會(huì)被卡死。 -
Go 的調(diào)度方式
協(xié)作式調(diào)度:依靠被調(diào)度放主動(dòng)棄權(quán)
搶占式調(diào)度:依靠調(diào)度器強(qiáng)制將被調(diào)度方中斷
s
Go的調(diào)度方式

- 小結(jié)

六、垃圾回收和Golang內(nèi)存管理
6.1 GC基本理論
- 自動(dòng)內(nèi)存管理:Reference Counting 引用計(jì)數(shù)法;Tracing GC
- Tracing GC:
- 目標(biāo):找出活的對(duì)象,剩下的就是垃圾。
- 兩部分:GC root 和 GC heap。
- 風(fēng)格:Copying GC 和 Mark-Sweep GC。
- 并發(fā):Concurrent GC(GC過(guò)程用戶代碼不需要停下來(lái)) 和 Parallel GC(GC過(guò)程中用戶代碼暫停)
6.2 Go內(nèi)存管理

歷史
- Go 1.10 版本以前采用的方式是線性內(nèi)存。所有申請(qǐng)的內(nèi)存以Page方式分割,每個(gè)Span管理一個(gè)或者多個(gè)Page。Golang垃圾回收的時(shí)候,會(huì)通過(guò)判斷指針的地址來(lái)判斷對(duì)象是否在堆中。之后棄用原因:1. 在C,Go混用時(shí),分配的內(nèi)存地址會(huì)發(fā)生沖突,導(dǎo)致堆得初始化和擴(kuò)容失?。?. 沒有被預(yù)留的大塊內(nèi)存可能會(huì)被分配給 C 語(yǔ)言,導(dǎo)致擴(kuò)容后的堆不連續(xù)。
- Go 1.10 之后采用稀疏內(nèi)存管理。
分配
-
關(guān)鍵詞
- Tcmalloc 風(fēng)格分配器:thread cache 分配器、三級(jí)內(nèi)存管理
- 按照不同大小分類:Tiny(8), Small(16~32K), Huge(>32K)
- mcache 來(lái)減輕鎖的開銷(每個(gè)處理器 P 維護(hù)一段內(nèi)存)
- 內(nèi)外部碎片
- Object 定位
- Bitmap 標(biāo)記
-
總覽
- Go在程序啟動(dòng)時(shí),會(huì)向操作系統(tǒng)申請(qǐng)一大塊內(nèi)存,之后自行管理。
- Go內(nèi)存管理的基本單元是mspan,它由若干個(gè)頁(yè)組成,每種mspan可以分配特定大小的object。
mcache, mcentral, mheap是Go內(nèi)存管理的三大組件,層層遞進(jìn)。mcache管理線程在本地緩存的mspan;mcentral管理全局的mspan供所有線程使用;mheap管理Go的所有動(dòng)態(tài)分配內(nèi)存。 - 極小對(duì)象(小于16字節(jié))會(huì)分配在一個(gè)object中,以節(jié)省資源,使用tiny分配器分配內(nèi)存;一般小對(duì)象(16字節(jié)到32768字節(jié))通過(guò)mspan分配內(nèi)存,根據(jù)對(duì)象大小選擇對(duì)應(yīng)的額mspan;大對(duì)象(大于32768字節(jié))則直接由mheap分配內(nèi)存,并記錄 spanClass=0。
- 微對(duì)象 (0, 16B) — 先使用微型分配器,再依次嘗試線程緩存、中心緩存和堆分配內(nèi)存;(注:對(duì)于(0, 16B) 的指針對(duì)象,直接歸類為小對(duì)象。微型分配器不分配指針類型對(duì)象)
- 小對(duì)象 [16B, 32KB] — 依次嘗試使用線程緩存、中心緩存和堆分配內(nèi)存;
- 大對(duì)象 (32KB, +∞) — 直接在堆上分配內(nèi)存;
-
詳解
與TCMalloc非常類似.Golang內(nèi)存分配由mspan,mcache,mcentral,mheap組成??梢哉f(shuō)基本對(duì)應(yīng)了TCMalloc中的Span,Pre-Thread,Central Free List,以及Page Heap。分配邏輯也很像TCMalloc中依次向前端,中端,后端請(qǐng)求內(nèi)存。

- 在Golang的程序中,每個(gè)處理器都會(huì)分配一個(gè)線程緩存 mcache 用于處理微對(duì)象以及小對(duì)象的內(nèi)存分配,mcache管理的單位就是mspan。
- mcache會(huì)被綁定在并發(fā)模型中的 P 上.也就是說(shuō)每一個(gè) P(處理器) 都會(huì)有一個(gè)mcache,用于給對(duì)應(yīng)的協(xié)程的對(duì)象分配內(nèi)存;
- mspan 是真正的內(nèi)存管理單元,其根據(jù)定義的 67 種 spanClass 來(lái)管理內(nèi)存(從8bytes到32768bytes==32KB),不同大小的對(duì)象,向上取整到對(duì)應(yīng)的 spanClass 中管理。
type spanClass uint8,其實(shí) spanClass 的載體就是一個(gè)8位的數(shù)據(jù),他的前七位用于存儲(chǔ)當(dāng)前 mspan 屬于68種的哪一種,最后一位代表當(dāng)前 mspan(當(dāng)前對(duì)象) 是否存儲(chǔ)了指針,這個(gè)非常重要,因?yàn)槭欠翊嬖谥羔樢馕吨欠裥枰诶厥盏臅r(shí)候進(jìn)行掃描; - mcache中的緩存對(duì)象數(shù)組
alloc [numSpanClasses]*mspan一共有(67) * 2個(gè),其中*2是將spanClass分成了有指針和沒有指針兩種,方便與垃圾回收;
- 如果mcache中緩存的對(duì)象數(shù)量不夠了,也就是alloc數(shù)組中緩存的對(duì)象不足,會(huì)向mheap持有的 numSpanClasses*2 個(gè)mcentral獲取新的內(nèi)存單元(這里的 *2 也是mcache中的 *2,對(duì)應(yīng)了無(wú)指針和有指針)
- 每個(gè) mcentral 維護(hù)一種 mspan,而 mspan 的種類會(huì)導(dǎo)致其分割的 object 大小不同。mcentral 被所有的工作線程共同享有,存在多個(gè)Goroutine競(jìng)爭(zhēng)的情況,因此會(huì)消耗鎖資源;
- mcache向 mcentral 申請(qǐng)空間的方法
mheap_.central[spc].mcentral.cacheSpan()
- mcentral中心緩存是屬于全局結(jié)構(gòu)mheap的,mheap就是用來(lái)管理Golang所申請(qǐng)的所有內(nèi)存,如果mheap的內(nèi)存也不夠,則會(huì)向操作系統(tǒng)申請(qǐng)內(nèi)存
- heapArena用于管理真實(shí)的內(nèi)存
回收

- STW Mark
- Concurrent mark
- Mark-Sweep
- 三色法 黑灰白:黑 標(biāo)活且內(nèi)容全部掃描完;灰 標(biāo)活且內(nèi)容未掃描完;白 未掃描到
- Non-generational
何時(shí)觸發(fā)回收
- GOGC threshold。閾值,假設(shè)設(shè)定 export GOGC=100,那么每次GC結(jié)束后,剩余活對(duì)象的內(nèi)存占用空間的兩倍(1+$(GOGC)%)作為下次GC的閾值,達(dá)到或者超過(guò),則啟動(dòng)GC
- runtime.GC()
- runtime.forcegcperiod(2min)
3.編程者指南
六、性能 pprof 工具

- 使用方式:
go tool pprof -http=:8080。輸入網(wǎng)頁(yè)查看:http://localhost:6060/debug/pprof- 網(wǎng)頁(yè)后綴
/profile查看 CPU 采樣信息 - 網(wǎng)頁(yè)后綴
/heap查看 堆占用 采樣信息
- 網(wǎng)頁(yè)后綴
七、緩存相關(guān)
1. local cache


大key問題:
- 考慮拆分成多個(gè)key來(lái)存儲(chǔ)
- 比如用hash取余/位掩碼的方式?jīng)Q定放在哪個(gè)key中
- 對(duì)于需要全量數(shù)據(jù)的場(chǎng)景,會(huì)增加一定數(shù)據(jù)請(qǐng)求和組裝的成本
- 考慮拆分冷熱數(shù)據(jù)
- redis中只存儲(chǔ)熱數(shù)據(jù),對(duì)于命中率不高的冷數(shù)據(jù),使用其他異構(gòu)數(shù)據(jù)庫(kù)
- 如粉絲列表場(chǎng)景使用zset,只緩存前10頁(yè)數(shù)據(jù),后續(xù)走db/hbase
推薦閱讀:
《redis redlock 是否可靠?》
八、內(nèi)存對(duì)齊
依次看:Golang 是否有必要做內(nèi)存對(duì)齊?、Golang 內(nèi)存對(duì)齊
簡(jiǎn)單總結(jié):對(duì)齊是因?yàn)镃PU不是支持任意字節(jié)獲取內(nèi)存的,而是一塊一塊獲取,所以對(duì)齊的好處是防止CPU需要兩次操作才能讀取數(shù)據(jù),從而降低效率。如果未對(duì)齊,則通過(guò)padding來(lái)補(bǔ)齊未對(duì)齊部分。x86 是4字節(jié)對(duì)齊,現(xiàn)在的64位系統(tǒng)通常是8字節(jié)對(duì)齊(比如 int64 剛好夠,int8 int32 單獨(dú)的就需要補(bǔ)齊,同時(shí)出現(xiàn)的話可以將 int32 補(bǔ)在 int8 后面,形成1字節(jié)int8,3字節(jié)padding,4字節(jié)int32的8字節(jié)對(duì)齊格式)。
Go Struct 偏移量還會(huì)內(nèi)存分配的知識(shí)點(diǎn):比如 SpanClass 的選定也會(huì)影響到數(shù)據(jù)分配的偏移量。(32, 48] 字節(jié)的 struct 會(huì)使用 48 字節(jié)的 Span。因此即使是順序分配,也是 48 字節(jié)的 offset 間隔。
-
內(nèi)存對(duì)齊使用舉例:Go WaitGroup
state函數(shù)會(huì)判斷編譯器是否是8字節(jié)對(duì)齊來(lái)決定 waiter 計(jì)數(shù)器、counter 計(jì)數(shù)器以及信號(hào)量的排列順序。
type WaitGroup struct {
noCopy noCopy // 輔助vet工具檢查是否通過(guò)copy賦值WaitGroup
state1 [3]uint32 // 數(shù)組,組成 waiter 計(jì)數(shù)器、counter 計(jì)數(shù)器以及信號(hào)量
// counter 代表目前尚未完成的個(gè)數(shù)。WaitGroup.Add(n) 將會(huì)導(dǎo)致 counter += n, 而 WaitGroup.Done() 將導(dǎo)致 counter--。
// waiter 代表目前已調(diào)用 WaitGroup.Wait 的 goroutine 的個(gè)數(shù)。
// sema 對(duì)應(yīng)于 golang 中 runtime 內(nèi)部的信號(hào)量的實(shí)現(xiàn)。
// WaitGroup 中會(huì)用到 sema 的兩個(gè)相關(guān)函數(shù),runtime_Semacquire 和 runtime_Semrelease。
// runtime_Semacquire 表示增加一個(gè)信號(hào)量,并掛起 當(dāng)前 goroutine。
// runtime_Semrelease 表示減少一個(gè)信號(hào)量,并喚醒 sema 上其中一個(gè)正在等待的 goroutine
}
func (wg *WaitGroup) state() (statep *uint64, semap *uint32) {
if uintptr(unsafe.Pointer(&wg.state1))%8 == 0 { // 8字節(jié)對(duì)齊
return (*uint64)(unsafe.Pointer(&wg.state1)), &wg.state1[2]
} else { // 4字節(jié)對(duì)齊
return (*uint64)(unsafe.Pointer(&wg.state1[1])), &wg.state1[0]
}
}

- Add 操作
使用規(guī)范:默認(rèn)使用者傳入的 delta 為正數(shù),使用者不應(yīng)該傳入 Add 函數(shù)一個(gè)負(fù)數(shù)
func (wg *WaitGroup) Add(delta int) {
statep, semap := wg.state()
// delta左移32位,將delta原子添加到高位計(jì)數(shù)器上
state := atomic.AddUint64(statep, uint64(delta)<<32)
v := int32(state >> 32) // 右移32位,獲取高32位計(jì)數(shù)器,因?yàn)?v 存在被 delta 操作,所以可能為負(fù)數(shù)。
w := uint32(state) // 高位截?cái)?,獲取低32位Waiter計(jì)數(shù)器,w 只可能在 Wait 函數(shù)中被 atomic +1,不可能為負(fù)數(shù)
if v < 0 { // 計(jì)數(shù)器不能小于0(使用者非預(yù)估:調(diào)用 Add 加了負(fù)數(shù))
panic("sync: negative WaitGroup counter")
}
// 計(jì)數(shù)器數(shù)據(jù)不一致,計(jì)數(shù)器和delta一樣的情況下,waiter 不是 0(使用者非預(yù)估:調(diào)用 Add 且調(diào)用 Wait 時(shí),又調(diào)用 Add,導(dǎo)致并發(fā)問題)
if w != 0 && delta > 0 && v == int32(delta) {
panic("sync: WaitGroup misuse: Add called concurrently with Wait")
}
if v > 0 || w == 0 { // 正確add,return
return
}
// 走到這里,說(shuō)明 v==0 && w>0 (v<0被panic,v>0被返回,w==0被返回)
if *statep != state { // state數(shù)值發(fā)生不一致(使用者非預(yù)估:v==0時(shí),w發(fā)生變化,說(shuō)明在Add調(diào)用過(guò)程中,Wait或者Add被非預(yù)估調(diào)用)
panic("sync: WaitGroup misuse: Add called concurrently with Wait")
}
*statep = 0 // 直接至零,表明 v==0 且 w==0,同時(shí)喚醒所有的 wait 狀態(tài)的 goroutine,只有最后一個(gè) Done 的 goroutine 會(huì)這么做
for ; w != 0; w-- { // 依次喚醒
runtime_Semrelease(semap, false, 0) // 釋放信號(hào)量
}
}
- Done 操作
預(yù)期內(nèi)只有調(diào)用 Done 時(shí),才會(huì)調(diào)用 Add 并傳入負(fù)值
// Done decrements the WaitGroup counter by one.
func (wg *WaitGroup) Done() {
wg.Add(-1)
}
- Wait 操作
通過(guò)自旋和樂觀鎖,保證計(jì)數(shù)器正確被更新
func (wg *WaitGroup) Wait() {
statep, semap := wg.state()
for { // 自旋循環(huán)
state := atomic.LoadUint64(statep)
v := int32(state >> 32) // 右移32位,獲取高32位計(jì)數(shù)器
w := uint32(state) // 高位截?cái)?,獲取低32位Waiter計(jì)數(shù)器
if v == 0 { // 計(jì)數(shù)器為0,不需要繼續(xù)等待
return
}
// 如果計(jì)數(shù)器不為0,調(diào)用wait方法的goroutine需要等待,等待計(jì)數(shù)器+1,并發(fā)調(diào)用安全
// 如果 state 發(fā)生了變化,則自旋,并重新觀測(cè) counter 并更新 waiter
if atomic.CompareAndSwapUint64(statep, state, state+1) {
runtime_Semacquire(semap) // 獲取信號(hào)量
// 被喚醒時(shí),肯定是最后一個(gè) goroutine Done,并依次喚醒所有的在 Wait 的 goroutine,此過(guò)程中不期望 state 發(fā)生變化(即存在并發(fā)的 Done 操作或者 Add 操作或者 Wait 操作)
if *statep != 0 { // 在wait返回前,WaitGroup被重用了(不期望的事情發(fā)生了)
panic("sync: WaitGroup is reused before previous Wait has returned")
}
return
}
}
}
- 思考:
- 把 waiter 和 counter 合并成一個(gè)變量:
為了避免使用鎖,直接利用 atomic 操作,保證兩者的改動(dòng)是同時(shí)的。比如Wait時(shí)通過(guò)樂觀鎖進(jìn)行操作,如果同時(shí)Done或者Wait被調(diào)用,那么會(huì)自旋重新觀測(cè)v,如果v==0則直接返回,否則 waiter 計(jì)數(shù)+1且進(jìn)入掛起等待。 - 沖突考慮(這里的 Add 表示 Add正值,Add負(fù)值的情況視為Done):Add 和 Wait 不能并發(fā),Add 和 Done 可以并發(fā),Wait 和 Done 可以并發(fā)。
- 把 waiter 和 counter 合并成一個(gè)變量:
- 使用規(guī)范:
- Add 不能和 Wait 并發(fā)調(diào)用,必須由一個(gè) goroutine 調(diào)用這兩個(gè)函數(shù)。
- 不應(yīng)該 Add 一個(gè)負(fù)值。Done 即為 Add(-1)。
- 不能在 Add 之前調(diào)用 Done。

