Sync包源碼解析:Cond

版本

  • go version 1.10.1

使用方法

//創(chuàng)建Cond
cond := sync.NewCond(new(sync.Mutex))
//等待喚醒
cond.L.Lock()
cond.Wait()
//喚醒一個
cond.Signal()
//喚醒所有
cond.Broadcast()

數據結構

/*
package: sync
file: cond.go
line: 21
*/

type Cond struct {
    noCopy noCopy
    L Locker
    notify  notifyList
    checker copyChecker
}

  • noCopy:noCopy對象全蝶,擁有一個Lock方法菜循,使得Cond對象在進行go vet掃描的時候体谒,能夠被檢測到是否被復制优烧。

    /*
    package: sync
    file: cond.go
    line: 94
    */
    type noCopy struct{}  
    
    func (*noCopy) Lock() {}
    
    
  • L:實現了Locker接口的鎖對象却妨,通常使用Mutex或RWMutex青责。

    /*
    package: sync
    file: mutex.go
    line: 31
    */
    type Locker interface {
         Lock()
         Unlock()
    }
    
  • notify:notifyList對象扣囊,維護等待喚醒的goroutine隊列,使用鏈表實現饼灿。

    /*
    package: sync
    file: runtime.go
    line: 29    
    */
    type notifyList struct {
         wait   uint32
         notify uint32
         lock   uintptr
         head   unsafe.Pointer
         tail   unsafe.Pointer
    }    
    
  • checker:copyChecker對象夷家,實際上是uintptr對象蒸其,保存自身對象地址。

    /*
    package: sync
    file: cond.go
    line: 79    
    */    
    type copyChecker uintptr
    
    func (c *copyChecker) check() {
         if uintptr(*c) != uintptr(unsafe.Pointer(c)) &&
                !atomic.CompareAndSwapUintptr((*uintptr)(c), 0, uintptr(unsafe.Pointer(c))) &&
                uintptr(*c) != uintptr(unsafe.Pointer(c)) {
                panic("sync.Cond is copied")
         }
    }
    
    1. 檢查當前checker的地址是否等于保存在checker中的地址
    2. 對checker進行原子CAS操作库快,將checker當前地址賦值給為空的checker
    3. 重復操作1摸袁,防止在進行1和2的時候,有其他gorountine并發(fā)的修改了checker值
    4. 若1义屏、2靠汁、3都不滿足,則表示當前cond是復制的闽铐,拋出panic

    check方法在第一次調用的時候蝶怔,會將checker對象地址賦值給checker,也就是將自身內存地址賦值給自身兄墅。
    再次調用checker方法的時候踢星,會將當前checker地址的值與保存的checker地址值進行比較,若不相同則表示當前checker的地址不是第一次調用check方法時候的地址隙咸,即cond對象被復制了沐悦,checker被重新分配了內存地址。

方法

NwoCond

/*
package: sync
file: cond.go
line: 32    
*/    
func NewCond(l Locker) *Cond {
    return &Cond{L: l}
}

NewCond方法傳入一個實現了Locker接口的對象五督,返回一個新的Cond對象指針藏否,保證在多goroutine使用cond的時候,持有的是同一個實例充包。

Wait

/*
package: sync
file: cond.go
line: 52    
*/
func (c *Cond) Wait() {
    c.checker.check() //step 1
    t := runtime_notifyListAdd(&c.notify) //step 2
    c.L.Unlock() //step 3
    runtime_notifyListWait(&c.notify, t) //step 4
    c.L.Lock() //step 5
}
  1. check檢查副签,保證cond在第一次使用后沒有被復制
  2. 將notify隊列的等待數加1,并將之前的等待數返回
     /*
     package: runtime
     file: sema.go
     line: 476  
     */
     func notifyListAdd(l *notifyList) uint32 {
         return atomic.Xadd(&l.wait, 1) - 1
     }
    
  3. 釋放鎖,因此在調用Wait方法前,必須保證獲取到了cond的鎖继薛,否則會報錯
  4. 將當前goroutine掛起,等待喚醒信號
    /*
    package: runtime
    file: sema.go
    line: 485  
    */
    func notifyListWait(l *notifyList, t uint32) {
       lock(&l.lock) //step a
    
       if less(t, l.notify) { //step b
           unlock(&l.lock)
           return
       }
    
       s := acquireSudog() //step c
       s.g = getg()
       s.ticket = t
       s.releasetime = 0
       t0 := int64(0)
       if blockprofilerate > 0 {
           t0 = cputicks()
           s.releasetime = -1
       }
       if l.tail == nil { //step d
           l.head = s
       } else {
           l.tail.next = s
       }
       l.tail = s
       goparkunlock(&l.lock, "semacquire", traceEvGoBlockCond, 3) //step e
       if t0 != 0 {
           blockevent(s.releasetime-t0, 2)
       }
       releaseSudog(s)
    }
    
    a. 鎖定notify隊列
    b. 如果notif隊列的等待數小于喚醒數 表示當前goroutine不需要再進行等待愈捅,則解鎖notify隊列遏考,直接返回
    c. 獲取當前goroutine,設置相關參數蓝谨,將當前等待數賦值給ticket
    d. 將當前goroutine加入到notify隊列中
    e. 將當前goroutine掛起灌具,等待喚醒信號

  5. gorountine被喚醒,重新獲取鎖

Signal

/*
package: sync
file: cond.go
line: 64  
*/
func (c *Cond) Signal() {
    c.checker.check() //step 1
    runtime_notifyListNotifyOne(&c.notify) //step 2
}
  1. check檢查譬巫,保證cond在第一次使用后沒有被復制

  2. 順序喚醒一個等待的gorountine

    /*
    package: runtime
    file: sema.go
    line: 485  
    */
    func notifyListNotifyOne(l *notifyList) {
    
       if atomic.Load(&l.wait) == atomic.Load(&l.notify) { //step a
            return
       }
    
       lock(&l.lock) //step b
    
       t := l.notify
       if t == atomic.Load(&l.wait) {
            unlock(&l.lock)
            return
       }
    
       atomic.Store(&l.notify, t+1)  
    
       for p, s := (*sudog)(nil), l.head; s != nil; p, s = s, s.next { //step c
            if s.ticket == t {
               n := s.next
               if p != nil {
                   p.next = n
               } else {
                   l.head = n
               }
               if n == nil {
                   l.tail = p
               }
               unlock(&l.lock)
               s.next = nil
               readyWithTime(s, 4)
               return
           }
       }
       unlock(&l.lock)
    }
    

    a. 如果notify隊列的等待數等于喚醒數咖楣,表示沒有新的goroutine在等待隊列上,不需要喚醒任何goroutine
    b. 鎖定notify隊列芦昔,對隊列進行雙檢查诱贿,查看是否有新的goroutine需要被喚醒,若無則將喚醒數加1
    c. 遍歷等待喚醒的goroutine隊列咕缎,喚醒ticket等于喚醒數的goroutine珠十,即順序喚醒一個最先加入等待隊列的goroutine

Broadcast

/*
package: sync
file: cond.go
line: 73  
*/
func (c *Cond) Broadcast() {
    c.checker.check()
    runtime_notifyListNotifyAll(&c.notify)
}
  1. check檢查,保證cond在第一次使用后沒有被復制

  2. 喚醒所有gorountine

    /*
    package: runtime
    file: sema.go
    line: 485  
    */
    func notifyListNotifyAll(l *notifyList) {
    
       if atomic.Load(&l.wait) == atomic.Load(&l.notify) { //step a
           return
       }
    
       lock(&l.lock) //step b
       s := l.head
       l.head = nil
       l.tail = nil
    
       atomic.Store(&l.notify, atomic.Load(&l.wait))
       unlock(&l.lock)
    
       for s != nil { //step c
           next := s.next
           s.next = nil
           readyWithTime(s, 4)
           s = next
       }
    }
    

    a. 如果notify隊列的等待數等于喚醒數凭豪,表示沒有新的goroutine在等待隊列上焙蹭,不需要喚醒任何goroutine
    b. 鎖定notify隊列,清空等待喚醒隊列嫂伞,將等待數賦值給喚醒數
    c. 遍歷等待喚醒的gorountine隊列孔厉,將所有goroutine喚醒

總結

  • Cond不能被復制:Cond在內部持有一個等待隊列,這個隊列維護所有等待在這個Cond的goroutine帖努。因此若這個Cond允許值傳遞撰豺,則這個隊列在值傳遞的過程中會進行復制,導致在喚醒goroutine的時候出現錯誤拼余。

  • 順序喚醒: notifyList對象持有兩個無限自增的字段wait和notify郑趁,wait字段在有新的goroutine等待的時候加1,notify字段在有新的喚醒信號的時候加1姿搜。在有新的goroutine加入隊列的時候寡润,會將當前wait賦值給goroutine的ticket,喚醒的時候會喚醒ticket等于notify的gourine舅柜。另外梭纹,當wait==notify時表示沒有goroutine需要被喚醒,wait>notify時表示有goroutine需要被喚醒致份,waity恒大于等于notify变抽。

?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子绍载,更是在濱河造成了極大的恐慌诡宗,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,692評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件击儡,死亡現場離奇詭異塔沃,居然都是意外死亡,警方通過查閱死者的電腦和手機阳谍,發(fā)現死者居然都...
    沈念sama閱讀 92,482評論 3 392
  • 文/潘曉璐 我一進店門蛀柴,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人矫夯,你說我怎么就攤上這事鸽疾。” “怎么了训貌?”我有些...
    開封第一講書人閱讀 162,995評論 0 353
  • 文/不壞的土叔 我叫張陵制肮,是天一觀的道長。 經常有香客問我递沪,道長弄企,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,223評論 1 292
  • 正文 為了忘掉前任区拳,我火速辦了婚禮拘领,結果婚禮上,老公的妹妹穿的比我還像新娘樱调。我一直安慰自己约素,他們只是感情好,可當我...
    茶點故事閱讀 67,245評論 6 388
  • 文/花漫 我一把揭開白布笆凌。 她就那樣靜靜地躺著圣猎,像睡著了一般。 火紅的嫁衣襯著肌膚如雪乞而。 梳的紋絲不亂的頭發(fā)上送悔,一...
    開封第一講書人閱讀 51,208評論 1 299
  • 那天,我揣著相機與錄音爪模,去河邊找鬼欠啤。 笑死,一個胖子當著我的面吹牛屋灌,可吹牛的內容都是我干的洁段。 我是一名探鬼主播,決...
    沈念sama閱讀 40,091評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼共郭,長吁一口氣:“原來是場噩夢啊……” “哼祠丝!你這毒婦竟也來了疾呻?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 38,929評論 0 274
  • 序言:老撾萬榮一對情侶失蹤写半,失蹤者是張志新(化名)和其女友劉穎岸蜗,沒想到半個月后,有當地人在樹林里發(fā)現了一具尸體叠蝇,經...
    沈念sama閱讀 45,346評論 1 311
  • 正文 獨居荒郊野嶺守林人離奇死亡璃岳,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,570評論 2 333
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現自己被綠了蟆肆。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,739評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡晦款,死狀恐怖炎功,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情缓溅,我是刑警寧澤蛇损,帶...
    沈念sama閱讀 35,437評論 5 344
  • 正文 年R本政府宣布,位于F島的核電站坛怪,受9級特大地震影響淤齐,放射性物質發(fā)生泄漏。R本人自食惡果不足惜袜匿,卻給世界環(huán)境...
    茶點故事閱讀 41,037評論 3 326
  • 文/蒙蒙 一更啄、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧居灯,春花似錦祭务、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,677評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至岩灭,卻和暖如春拌倍,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背噪径。 一陣腳步聲響...
    開封第一講書人閱讀 32,833評論 1 269
  • 我被黑心中介騙來泰國打工柱恤, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人找爱。 一個月前我還...
    沈念sama閱讀 47,760評論 2 369
  • 正文 我出身青樓膨更,卻偏偏與公主長得像,于是被迫代替她去往敵國和親缴允。 傳聞我的和親對象是個殘疾皇子荚守,可洞房花燭夜當晚...
    茶點故事閱讀 44,647評論 2 354

推薦閱讀更多精彩內容