Golang 進階訓練營

一冲粤、Golang 的 slice恢着、map途凫、channel

1.1 slice vs array

a := make([]int, 100) //切片
b := [100]int{} //數(shù)組

array需指明長度,長度為常量且不可改變
array長度為其類型中的組成部分(給參數(shù)為長度100的數(shù)組的方法傳長度為101的會報錯)
array在作為函數(shù)參數(shù)時會產(chǎn)生copy
golang所有函數(shù)參數(shù)都是值傳遞

array擴容:cap<1024時乘2而克,否則乘1.25靶壮,預先分配內(nèi)存可以提升性能,直接使用index賦值而不是append可以提升性能

slice作為參數(shù)被修改時员萍,如果沒有發(fā)生擴容腾降,修改在原來的內(nèi)存中;如果發(fā)生了擴容碎绎,修改會在新的內(nèi)存中螃壤。

使用[]Type{}或者make([]Type)初始化后,slice不為nil筋帖;使用var x[]Type后奸晴,slice為nil

1.2 Map:

map的值其實是指針,傳map傳的是指針日麸,所以修改會影響整個map寄啼。
map的k、v都不可取地址代箭,隨著map的擴容地址會改變墩划。map存的是值,會發(fā)生copy嗡综,因此不要在map里放很大的數(shù)組走诞,很大的可以用指針來代替
map賦值會自動擴容,但刪除時不會自動縮容蛤高。
map非線程安全蚣旱,不能同時讀寫。

1.3 Channel

有鎖
緩沖channel和非緩沖的區(qū)別戴陡,緩沖會發(fā)生兩次copy塞绿,非緩沖發(fā)生一次
for+select closed channel會造成死循環(huán),select中的break無法跳出for循環(huán)

二恤批、GOTT best practices

2.1 可讀性

  • if else 和 happy path:有錯誤應該提前返回异吻,盡量在正確返回時,不加 indents(縮進)喜庞。
  • init() 使用規(guī)范:在一些 package 中盡量不要使用 init()诀浪,定義一個可以被調(diào)用的 Initxxx() 函數(shù)顯示調(diào)用,防止運行一些使用方不知道的代碼段延都。
  • Comments:盡量寫函數(shù)做了什么雷猪,而不是怎么做的。

2.2 健壯性

  • panic: 在 defer 中進行 recover()
  • Errors:使用 errors.Is() 和 errors.As() 來判斷 error 和斷言 error
    相關文檔

2.3 效率

  1. 指針:函數(shù)修改參數(shù)晰房,應該傳遞指針求摇;參數(shù)中含有大量的內(nèi)容射沟,避免拷貝可以傳遞指針;代碼風格對齊与境,其他函數(shù)都是傳遞指針的验夯;
    Tricks:結構體默認傳遞指針;對于 for 中定義的變量在循環(huán)中會變摔刁,取其地址得到的值永遠是最后一個挥转。
  2. Deprecation:對于要廢棄的函數(shù),使用以下注釋格式共屈,從而使得 golintci-lint 能夠檢測而出來扁位。
// comments for the function
//
// Deprecated: use $funcName instead.
func funcToBeDeprecated(){
}

三、Golang 的強人鎖難

鎖的重要性:并發(fā)場景通過 goroutine 和 channel 來實現(xiàn)趁俊,但是 goroutine 之間可以共享內(nèi)存和變量,導致直接修改變量的時候刑然,會存在沖突寺擂。使用鎖需要考慮的:性能、重入泼掠、公平

3.1 強人:最佳實踐

  • 減少持有時間怔软,縮小臨界區(qū)
    可以的情況下盡量提前釋放,或者新定義一個函數(shù)择镇,函數(shù)內(nèi)部執(zhí)行臨界區(qū)挡逼,以及上鎖釋放鎖,函數(shù)后進行其他的邏輯操作
  • 優(yōu)化鎖的粒度
    空間換時間腻豌,分片操作家坎,每個片加鎖。
  • 讀寫分離
    RWMutex吝梅;sync.Map(空間換時間)
  • 使用原子操作虱疏,避免使用鎖
    atomic

3.2 鎖難:避免踩坑

  • 不要拷貝Mutex
    golang 函數(shù)傳參是復制拷貝,需要傳入指針
  • 鎖不能重入
    防止死鎖苏携,一個 goroutine 兩次調(diào)用 lock 會導致死鎖
  • atomic.Value 誤用
    存入的應該是只讀對象做瞪,如果存入一個 map,取出來對map操作右冻,那么map還是存在并發(fā)讀寫問題
  • 使用 race detector
    go test\run\build\install -race xxx:加上 race 參數(shù)装蓬,用來加強單測和壓測

3.3 暗黑:鎖的進化

  • 原子操作
    • 古代:英特爾 80386 處理器,因為是單核處理器纱扭,所以只需要鎖CPU牍帚,關閉中斷開關,這樣操作就不會被中斷乳蛾,操作完再打開中斷開關履羞。低效:需要內(nèi)核態(tài)來操作中斷開關
    • 近代:匯編代碼提供了 CMPXCHGL 指令峦萎,在該指令前加一個 Lock 前綴,會鎖定內(nèi)存總線忆首。低效:內(nèi)存總線稱為瓶頸
    • 現(xiàn)代:MESI 緩存一致性協(xié)議(降低鎖的粒度:總線鎖->緩存行鎖)爱榔。緩存行的狀態(tài),由硬件同步糙及。MESI 為 Modified, Exclusive, Shared, Invalid 縮寫详幽。
      • Invalid:無效。初始化狀態(tài)浸锨,或者內(nèi)存不可用(被其他CPU修改唇聘,需要更新緩存)
      • Exclusive:獨占。僅當前CPU緩存了該內(nèi)存柱搜。
      • Shared:共享迟郎。多個CPU緩存了該內(nèi)存。
      • Modified:已修改聪蘸、未寫回宪肖。需要其他CPU的緩存失效。某個CPU更新了緩存健爬,但是還沒寫回內(nèi)存控乾,其他CPU緩存的該內(nèi)存信息更新為 Invalid。
狀態(tài)轉(zhuǎn)移圖
  • 自旋鎖(Spin Lock)
    • Linux 內(nèi)核中常見娜遵,適合等待時間比較小的場景
    • Go 1.14 版本之前蜕衡,沒有實現(xiàn)搶占式調(diào)度,必須某個 goroutine 交出控制權设拟,因此自旋鎖會導致死鎖慨仿。如下圖:A等待B釋放鎖,但是執(zhí)行了GC纳胧,然后B被掛起镶骗,runtime需要等待A掛起,但是A在執(zhí)行自旋鎖躲雅,就發(fā)生了死鎖鼎姊。需要在自旋鎖內(nèi)部調(diào)用一次 runtime.GoSched 來交出 CPU 控制權
自旋鎖死鎖樣例
  • Go's Mutex
    • 效率優(yōu)先,兼顧公平相赁。
    • Mutex 有自己的一個等待隊列相寇,有自己的狀態(tài) state(正常模式和饑餓模式)。正常模式保證效率钮科,饑餓模式保證公平唤衫。state是一個共用字段,由鎖標志位绵脯,喚醒標志位佳励,饑餓標志位和阻塞的goroutine個數(shù)組成休里。


      Go's Mutex State 字段組成(mutexLocked mutexWoken mutexStarving 位為 1 分別表示鎖占用、鎖喚醒赃承、饑餓模式妙黍、mutexWaiterShift 表示偏移量,默認為3瞧剖,state>>=mutexWaiterShift拭嫁,state的值就表示當前阻塞等待鎖的goroutine個數(shù)。最多可以阻塞2^29個goroutine)
    • 正常模式:goroutine等待隊列先進先出抓于;新來的goroutine先去搶占鎖做粤,失敗了再進入等待隊列;如果發(fā)現(xiàn)某個搶到鎖的 goroutine 等待時長 > 1ms捉撮,則切換到饑餓模式怕品。
    • 饑餓模式:嚴格排隊,隊首接盤巾遭;犧牲效率肉康,保證Pct99;適時回歸正常模式恢总,保證效率。如果某個goroutine加鎖成功后睬愤,如果發(fā)現(xiàn)這個goroutine位于隊尾片仿,或者等待時間小于1ms,那么就切換回正常模式尤辱。
    • 提高效率的點:1. 新來的先去搶鎖砂豌,減少了調(diào)度開銷。2. 充分利用緩存光督,提高執(zhí)行效率阳距。
加鎖流程
state位定義
自旋流程
加鎖流程
解鎖流程1(Slow)
解鎖流程2
總結
  • 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()
    }
}
  • 源碼很簡單,如上:

    • 問題:1. 為什么 Do 里面用 atomic结借,doSlow 里面用 o.done==0筐摘?2. 為什么 doSlow 里面用 atomic 來設置 done?3. 為什么 doSlow 里面用 defer 設置值船老?可否直接設置咖熟?
    • 解答:1. Do 里面沒有加鎖,如果直接 o.done == 0 可能觀測到非常規(guī)值柳畔,使用 atomic 保證操作有序馍管。而 doSlow 里面已經(jīng)是鎖內(nèi)部,不可能存在其他的 goroutine 修改值薪韩,因此可以直接觀測确沸。2. 由于可能存在其他 goroutine 在 Do 內(nèi)觀測 done 的值捌锭,因此需要 atomic 設置值來保證有序性。3. 不可以直接設置罗捎,如果直接設置值再執(zhí)行f观谦,那么可能 f 還沒執(zhí)行,別的 goroutine 已經(jīng)觀測到 done 為 1宛逗,直接 Do 中返回坎匿,但是由于 f 的初始化函數(shù)還沒完成,從而導致 panic(空指針等)雷激。因此在 f 未執(zhí)行完的過程中替蔬,所有執(zhí)行 once.Do 的 goroutine 都被阻塞在 doSlow 的 Lock 階段,等待 f 執(zhí)行完成才可以返回屎暇。
    • 異常:假設 Do 使用 o.done == 0 來觀測值承桥,讀取的同時當 atomic 正在修改值時,讀取到的值可能是異常值根悼;假設使用 o.done=1 來設定值凶异,執(zhí)行的同時當其他 goroutine 在 Do 中讀取 o.done 時,可能看到異常值挤巡。
    • 總結:這里的兩個 atomic 是為了保證多個 goroutine 觀測和設定同時發(fā)生的有序性剩彬。而鎖操作的臨界區(qū)內(nèi)可以直接觀測變量值。
  • Go's WaitGroup
    下文 第八部分矿卑。巧妙地避免了鎖的使用喉恋。

  • 鎖的進化總結
    單核:關中斷->CAS指令
    多核:LOCK內(nèi)存總線->MESI協(xié)議
    自旋鎖:效率和公平不夠好
    Go Mutex:效率優(yōu)先,兼顧公平母廷。

  • 思考探索

思考探索

四、Golang 并發(fā)數(shù)據(jù)結構和算法實踐

引言

scalable:當計算資源更多時琴昆,性能會有提升


Golang數(shù)據(jù)結構并發(fā)測試(-x代表使用的CPU數(shù))

4.1 并發(fā)安全問題

  • Data Race
    原因:多個 goroutine 同時接觸一個變量氓鄙,行為不可預知。
    認定條件:兩個及以上 goroutine
    一寫多讀:atomic
    多寫一讀:Lock + atomic
    多寫多讀:Lock + atomic

4.2 實踐一:有序鏈表并行化

定義插入刪除的多個步驟业舍,舉例不同場景抖拦,考慮并發(fā)情況下是否滿足,如何調(diào)整步驟舷暮。

4.3 實踐二:skiplist并行化

從 4.2 延伸過來蟋座,每一層的 list 都可以使用 4.2 的實現(xiàn)。

4.4 總結

五脚牍、Golang 并發(fā)數(shù)據(jù)結構和算法實踐

5.1 調(diào)度循環(huán)的建立

  • GM 模型和 GMP 模型
  • 調(diào)度循環(huán)的建立
調(diào)度循環(huán)的建立

5.2 協(xié)作與搶占

  • 調(diào)度器的坑
    go 運行一個死循環(huán)在 go1.13 版本會被卡死向臀,go1.14 引入基于信號的搶占,從而不會被卡死诸狭。
  • Go 的調(diào)度方式
    協(xié)作式調(diào)度:依靠被調(diào)度放主動棄權
    搶占式調(diào)度:依靠調(diào)度器強制將被調(diào)度方中斷
    s


    Go的調(diào)度方式
基于信號的搶占
  • 小結
協(xié)作與搶占小結

六券膀、垃圾回收和Golang內(nèi)存管理

6.1 GC基本理論

  • 自動內(nèi)存管理:Reference Counting 引用計數(shù)法君纫;Tracing GC
  • Tracing GC:
    • 目標:找出活的對象,剩下的就是垃圾芹彬。
    • 兩部分:GC root 和 GC heap蓄髓。
    • 風格:Copying GC 和 Mark-Sweep GC。
    • 并發(fā):Concurrent GC(GC過程用戶代碼不需要停下來) 和 Parallel GC(GC過程中用戶代碼暫停)

6.2 Go內(nèi)存管理

Go GC簡史

歷史

  • Go 1.10 版本以前采用的方式是線性內(nèi)存舒帮。所有申請的內(nèi)存以Page方式分割会喝,每個Span管理一個或者多個Page。Golang垃圾回收的時候玩郊,會通過判斷指針的地址來判斷對象是否在堆中肢执。之后棄用原因:1. 在C,Go混用時,分配的內(nèi)存地址會發(fā)生沖突,導致堆得初始化和擴容失敗译红;2. 沒有被預留的大塊內(nèi)存可能會被分配給 C 語言预茄,導致擴容后的堆不連續(xù)。
  • Go 1.10 之后采用稀疏內(nèi)存管理侦厚。

分配

  • 關鍵詞
    • Tcmalloc 風格分配器:thread cache 分配器耻陕、三級內(nèi)存管理
    • 按照不同大小分類:Tiny(8), Small(16~32K), Huge(>32K)
    • mcache 來減輕鎖的開銷(每個處理器 P 維護一段內(nèi)存)
    • 內(nèi)外部碎片
    • Object 定位
    • Bitmap 標記
  • 總覽
    • Go在程序啟動時,會向操作系統(tǒng)申請一大塊內(nèi)存刨沦,之后自行管理诗宣。
    • Go內(nèi)存管理的基本單元是mspan,它由若干個頁組成想诅,每種mspan可以分配特定大小的object召庞。
      mcache, mcentral, mheap是Go內(nèi)存管理的三大組件,層層遞進侧蘸。mcache管理線程在本地緩存的mspan裁眯;mcentral管理全局的mspan供所有線程使用鹉梨;mheap管理Go的所有動態(tài)分配內(nèi)存讳癌。
    • 極小對象(小于16字節(jié))會分配在一個object中,以節(jié)省資源存皂,使用tiny分配器分配內(nèi)存晌坤;一般小對象(16字節(jié)到32768字節(jié))通過mspan分配內(nèi)存,根據(jù)對象大小選擇對應的額mspan旦袋;大對象(大于32768字節(jié))則直接由mheap分配內(nèi)存骤菠,并記錄 spanClass=0。
      • 微對象 (0, 16B) — 先使用微型分配器疤孕,再依次嘗試線程緩存商乎、中心緩存和堆分配內(nèi)存;(注:對于(0, 16B) 的指針對象祭阀,直接歸類為小對象鹉戚。微型分配器不分配指針類型對象)
      • 小對象 [16B, 32KB] — 依次嘗試使用線程緩存鲜戒、中心緩存和堆分配內(nèi)存;
      • 大對象 (32KB, +∞) — 直接在堆上分配內(nèi)存抹凳;
  • 詳解
    與TCMalloc非常類似.Golang內(nèi)存分配由mspan,mcache,mcentral,mheap組成遏餐。可以說基本對應了TCMalloc中的Span,Pre-Thread,Central Free List,以及Page Heap赢底。分配邏輯也很像TCMalloc中依次向前端,中端,后端請求內(nèi)存失都。
Golang內(nèi)存管理組件
  1. 在Golang的程序中,每個處理器都會分配一個線程緩存 mcache 用于處理微對象以及小對象的內(nèi)存分配,mcache管理的單位就是mspan。
    • mcache會被綁定在并發(fā)模型中的 P 上.也就是說每一個 P(處理器) 都會有一個mcache幸冻,用于給對應的協(xié)程的對象分配內(nèi)存粹庞;
    • mspan 是真正的內(nèi)存管理單元,其根據(jù)定義的 67 種 spanClass 來管理內(nèi)存(從8bytes到32768bytes==32KB)嘁扼,不同大小的對象信粮,向上取整到對應的 spanClass 中管理。type spanClass uint8趁啸,其實 spanClass 的載體就是一個8位的數(shù)據(jù),他的前七位用于存儲當前 mspan 屬于68種的哪一種,最后一位代表當前 mspan(當前對象) 是否存儲了指針,這個非常重要,因為是否存在指針意味著是否需要在垃圾回收的時候進行掃描强缘;
    • mcache中的緩存對象數(shù)組 alloc [numSpanClasses]*mspan 一共有(67) * 2個,其中*2是將spanClass分成了有指針和沒有指針兩種,方便與垃圾回收不傅;
  2. 如果mcache中緩存的對象數(shù)量不夠了,也就是alloc數(shù)組中緩存的對象不足,會向mheap持有的 numSpanClasses*2 個mcentral獲取新的內(nèi)存單元(這里的 *2 也是mcache中的 *2,對應了無指針和有指針)
    • 每個 mcentral 維護一種 mspan旅掂,而 mspan 的種類會導致其分割的 object 大小不同。mcentral 被所有的工作線程共同享有访娶,存在多個Goroutine競爭的情況商虐,因此會消耗鎖資源;
    • mcache向 mcentral 申請空間的方法 mheap_.central[spc].mcentral.cacheSpan()
  3. mcentral中心緩存是屬于全局結構mheap的,mheap就是用來管理Golang所申請的所有內(nèi)存,如果mheap的內(nèi)存也不夠,則會向操作系統(tǒng)申請內(nèi)存
  4. heapArena用于管理真實的內(nèi)存

回收

Go GC
  • STW Mark
  • Concurrent mark
  • Mark-Sweep
    • 三色法 黑灰白:黑 標活且內(nèi)容全部掃描完崖疤;灰 標活且內(nèi)容未掃描完秘车;白 未掃描到
  • Non-generational
何時觸發(fā)回收
  • GOGC threshold。閾值劫哼,假設設定 export GOGC=100叮趴,那么每次GC結束后,剩余活對象的內(nèi)存占用空間的兩倍(1+$(GOGC)%)作為下次GC的閾值权烧,達到或者超過眯亦,則啟動GC
  • runtime.GC()
  • runtime.forcegcperiod(2min)

3.編程者指南

六、性能 pprof 工具

pprof工具
  1. 使用方式:go tool pprof -http=:8080般码。輸入網(wǎng)頁查看:http://localhost:6060/debug/pprof
    • 網(wǎng)頁后綴 /profile 查看 CPU 采樣信息
    • 網(wǎng)頁后綴 /heap 查看 堆占用 采樣信息

七妻率、緩存相關

1. local cache

local cache 對比
local 選型

大key問題:

  1. 考慮拆分成多個key來存儲
    • 比如用hash取余/位掩碼的方式?jīng)Q定放在哪個key中
    • 對于需要全量數(shù)據(jù)的場景,會增加一定數(shù)據(jù)請求和組裝的成本
  2. 考慮拆分冷熱數(shù)據(jù)
    • redis中只存儲熱數(shù)據(jù)板祝,對于命中率不高的冷數(shù)據(jù)宫静,使用其他異構數(shù)據(jù)庫
    • 如粉絲列表場景使用zset,只緩存前10頁數(shù)據(jù),后續(xù)走db/hbase

推薦閱讀:
《redis redlock 是否可靠孤里?》

八温技、內(nèi)存對齊

依次看:Golang 是否有必要做內(nèi)存對齊?扭粱、Golang 內(nèi)存對齊
簡單總結:對齊是因為CPU不是支持任意字節(jié)獲取內(nèi)存的舵鳞,而是一塊一塊獲取,所以對齊的好處是防止CPU需要兩次操作才能讀取數(shù)據(jù)琢蛤,從而降低效率蜓堕。如果未對齊,則通過padding來補齊未對齊部分博其。x86 是4字節(jié)對齊套才,現(xiàn)在的64位系統(tǒng)通常是8字節(jié)對齊(比如 int64 剛好夠,int8 int32 單獨的就需要補齊慕淡,同時出現(xiàn)的話可以將 int32 補在 int8 后面背伴,形成1字節(jié)int8,3字節(jié)padding,4字節(jié)int32的8字節(jié)對齊格式)。
Go Struct 偏移量還會內(nèi)存分配的知識點:比如 SpanClass 的選定也會影響到數(shù)據(jù)分配的偏移量峰髓。(32, 48] 字節(jié)的 struct 會使用 48 字節(jié)的 Span傻寂。因此即使是順序分配,也是 48 字節(jié)的 offset 間隔携兵。

  • 內(nèi)存對齊使用舉例:Go WaitGroup
    state函數(shù)會判斷編譯器是否是8字節(jié)對齊來決定 waiter 計數(shù)器疾掰、counter 計數(shù)器以及信號量的排列順序。
type WaitGroup struct {
   noCopy noCopy      // 輔助vet工具檢查是否通過copy賦值WaitGroup
   state1 [3]uint32   // 數(shù)組徐紧,組成 waiter 計數(shù)器静檬、counter 計數(shù)器以及信號量
// counter 代表目前尚未完成的個數(shù)。WaitGroup.Add(n) 將會導致 counter += n, 而 WaitGroup.Done() 將導致 counter--并级。
// waiter 代表目前已調(diào)用 WaitGroup.Wait 的 goroutine 的個數(shù)拂檩。
// sema 對應于 golang 中 runtime 內(nèi)部的信號量的實現(xiàn)。
//   WaitGroup 中會用到 sema 的兩個相關函數(shù)嘲碧,runtime_Semacquire 和 runtime_Semrelease稻励。
//   runtime_Semacquire 表示增加一個信號量,并掛起 當前 goroutine呀潭。
//   runtime_Semrelease 表示減少一個信號量钉迷,并喚醒 sema 上其中一個正在等待的 goroutine
}
func (wg *WaitGroup) state() (statep *uint64, semap *uint32) {
   if uintptr(unsafe.Pointer(&wg.state1))%8 == 0 { // 8字節(jié)對齊
      return (*uint64)(unsafe.Pointer(&wg.state1)), &wg.state1[2]
   } else { // 4字節(jié)對齊
      return (*uint64)(unsafe.Pointer(&wg.state1[1])), &wg.state1[0]
   }
}
Go WaitGroup state 字段含義(如果是8字節(jié)對齊至非,也就是滿足第一個 if钠署,那么使用前兩個 uint32 組成一個 uint64 來返回,根據(jù)移位操作確認 waiter 和 counter 值荒椭,如果是4字節(jié)對齊谐鼎,那么 if 條件判斷失敗,使用后兩個 uint32 組成一個 uint64 返回趣惠。當然狸棍,這里 4 字節(jié)對齊的時候身害,也可能 state1 剛好處于 8 字節(jié)對齊的位置,那么會按照 8 字節(jié)對齊處理草戈,這主要取決于內(nèi)存分配時的具體情況塌鸯。)
  1. Add 操作
    使用規(guī)范:默認使用者傳入的 delta 為正數(shù),使用者不應該傳入 Add 函數(shù)一個負數(shù)
func (wg *WaitGroup) Add(delta int) {
   statep, semap := wg.state()
   // delta左移32位唐片,將delta原子添加到高位計數(shù)器上
   state := atomic.AddUint64(statep, uint64(delta)<<32) 
   v := int32(state >> 32)    // 右移32位丙猬,獲取高32位計數(shù)器,因為 v 存在被 delta 操作费韭,所以可能為負數(shù)茧球。
   w := uint32(state)         // 高位截斷,獲取低32位Waiter計數(shù)器星持,w 只可能在 Wait 函數(shù)中被 atomic +1抢埋,不可能為負數(shù)
   if v < 0 {                 // 計數(shù)器不能小于0(使用者非預估:調(diào)用 Add 加了負數(shù))
      panic("sync: negative WaitGroup counter")
   }
   // 計數(shù)器數(shù)據(jù)不一致,計數(shù)器和delta一樣的情況下督暂,waiter 不是 0(使用者非預估:調(diào)用 Add 且調(diào)用 Wait 時揪垄,又調(diào)用 Add,導致并發(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
   }
   // 走到這里福侈,說明 v==0 && w>0 (v<0被panic,v>0被返回卢未,w==0被返回)
   if *statep != state { // state數(shù)值發(fā)生不一致(使用者非預估:v==0時肪凛,w發(fā)生變化,說明在Add調(diào)用過程中辽社,Wait或者Add被非預估調(diào)用)
      panic("sync: WaitGroup misuse: Add called concurrently with Wait")
   }
   *statep = 0 // 直接至零伟墙,表明 v==0 且 w==0,同時喚醒所有的 wait 狀態(tài)的 goroutine滴铅,只有最后一個 Done 的 goroutine 會這么做
   for ; w != 0; w-- { // 依次喚醒
      runtime_Semrelease(semap, false, 0) // 釋放信號量
   }
}
  1. Done 操作
    預期內(nèi)只有調(diào)用 Done 時戳葵,才會調(diào)用 Add 并傳入負值
// Done decrements the WaitGroup counter by one.
func (wg *WaitGroup) Done() {
    wg.Add(-1)
}
  1. Wait 操作
    通過自旋和樂觀鎖,保證計數(shù)器正確被更新
func (wg *WaitGroup) Wait() {
   statep, semap := wg.state()
   for { // 自旋循環(huán)
      state := atomic.LoadUint64(statep)
      v := int32(state >> 32)  // 右移32位汉匙,獲取高32位計數(shù)器
      w := uint32(state)       // 高位截斷拱烁,獲取低32位Waiter計數(shù)器
      if v == 0 {              // 計數(shù)器為0,不需要繼續(xù)等待
         return
      }
      // 如果計數(shù)器不為0噩翠,調(diào)用wait方法的goroutine需要等待戏自,等待計數(shù)器+1,并發(fā)調(diào)用安全
      // 如果 state 發(fā)生了變化伤锚,則自旋擅笔,并重新觀測 counter 并更新 waiter
      if atomic.CompareAndSwapUint64(statep, state, state+1) {
         runtime_Semacquire(semap) // 獲取信號量
         // 被喚醒時,肯定是最后一個 goroutine Done,并依次喚醒所有的在 Wait 的 goroutine猛们,此過程中不期望 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 合并成一個變量:
      為了避免使用鎖,直接利用 atomic 操作弯淘,保證兩者的改動是同時的绿店。比如Wait時通過樂觀鎖進行操作,如果同時Done或者Wait被調(diào)用庐橙,那么會自旋重新觀測v惯吕,如果v==0則直接返回,否則 waiter 計數(shù)+1且進入掛起等待怕午。
    • 沖突考慮(這里的 Add 表示 Add正值废登,Add負值的情況視為Done):Add 和 Wait 不能并發(fā),Add 和 Done 可以并發(fā)郁惜,Wait 和 Done 可以并發(fā)堡距。
  • 使用規(guī)范:
    • Add 不能和 Wait 并發(fā)調(diào)用,必須由一個 goroutine 調(diào)用這兩個函數(shù)兆蕉。
    • 不應該 Add 一個負值羽戒。Done 即為 Add(-1)篓叶。
    • 不能在 Add 之前調(diào)用 Done蝶锋。
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市吟秩,隨后出現(xiàn)的幾起案子包蓝,更是在濱河造成了極大的恐慌驶社,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,376評論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件测萎,死亡現(xiàn)場離奇詭異亡电,居然都是意外死亡,警方通過查閱死者的電腦和手機硅瞧,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,126評論 2 385
  • 文/潘曉璐 我一進店門份乒,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人腕唧,你說我怎么就攤上這事或辖。” “怎么了枣接?”我有些...
    開封第一講書人閱讀 156,966評論 0 347
  • 文/不壞的土叔 我叫張陵颂暇,是天一觀的道長。 經(jīng)常有香客問我月腋,道長蟀架,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,432評論 1 283
  • 正文 為了忘掉前任榆骚,我火速辦了婚禮片拍,結果婚禮上,老公的妹妹穿的比我還像新娘妓肢。我一直安慰自己捌省,他們只是感情好,可當我...
    茶點故事閱讀 65,519評論 6 385
  • 文/花漫 我一把揭開白布碉钠。 她就那樣靜靜地躺著纲缓,像睡著了一般。 火紅的嫁衣襯著肌膚如雪喊废。 梳的紋絲不亂的頭發(fā)上祝高,一...
    開封第一講書人閱讀 49,792評論 1 290
  • 那天,我揣著相機與錄音污筷,去河邊找鬼工闺。 笑死,一個胖子當著我的面吹牛瓣蛀,可吹牛的內(nèi)容都是我干的陆蟆。 我是一名探鬼主播,決...
    沈念sama閱讀 38,933評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼惋增,長吁一口氣:“原來是場噩夢啊……” “哼叠殷!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起诈皿,我...
    開封第一講書人閱讀 37,701評論 0 266
  • 序言:老撾萬榮一對情侶失蹤林束,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后稽亏,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體诊县,經(jīng)...
    沈念sama閱讀 44,143評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,488評論 2 327
  • 正文 我和宋清朗相戀三年措左,在試婚紗的時候發(fā)現(xiàn)自己被綠了依痊。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,626評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡怎披,死狀恐怖胸嘁,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情凉逛,我是刑警寧澤性宏,帶...
    沈念sama閱讀 34,292評論 4 329
  • 正文 年R本政府宣布,位于F島的核電站状飞,受9級特大地震影響毫胜,放射性物質(zhì)發(fā)生泄漏书斜。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,896評論 3 313
  • 文/蒙蒙 一酵使、第九天 我趴在偏房一處隱蔽的房頂上張望荐吉。 院中可真熱鬧,春花似錦口渔、人聲如沸样屠。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,742評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽痪欲。三九已至,卻和暖如春攻礼,著一層夾襖步出監(jiān)牢的瞬間业踢,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,977評論 1 265
  • 我被黑心中介騙來泰國打工礁扮, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留陨亡,地道東北人。 一個月前我還...
    沈念sama閱讀 46,324評論 2 360
  • 正文 我出身青樓深员,卻偏偏與公主長得像负蠕,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子倦畅,可洞房花燭夜當晚...
    茶點故事閱讀 43,494評論 2 348

推薦閱讀更多精彩內(nèi)容