Go-同步原语与锁(二)WaitGroup、Once与Cond

本文将讲解一下Go语言中的同步原语与锁。会阐述几种常见的锁,剖析其流程,然后针对每种同步原语举几个例子。由于文章比较长,为方便阅读,将这部分拆解为两部分。本文是第二部分 WaitGroup、Once与Cond。

环境: go version go1.8.7 darwin/amd64

1 WaitGroup

1.1 结构

type WaitGroup struct {
    noCopy noCopy

    // 64-bit value: high 32 bits are counter, low 32 bits are waiter count.
    // 64-bit atomic operations require 64-bit alignment, but 32-bit
    // compilers do not ensure it. So we allocate 12 bytes and then use
    // the aligned 8 bytes in them as state.
    state1 [12]byte
    sema   uint32
}

如源码注释中所说,WaitGroup 就是要等一堆goroutines结束。主routine 利用wg的Add设置一个数量去等待,然后每个routine 执行,执行完调用wg的Done函数。同时,Wait在所有routine没有执行完之前,一直在等待。
其主要的函数就是Add, Done, Wait。

1.2 流程

1.2.1 Add与Done

Add 方法的主要作用就是更新 WaitGroup 中持有的计数器 counter。开始设置的是大于0的数量,当每次调用Done的时候,底层就是调用的Add方法,对计数器进行减一操作。当调用 Add 方法导致计数器归零并且还有等待的 Goroutine 时,就会通过 runtime_Semrelease 唤醒处于等待状态的所有 Goroutine

1.2.2 Wait

另一个 WaitGroup 的方法 Wait 就会在当前计数器中保存的数据大于 0 时修改等待 Goroutine 的个数 waiter 并调用 runtime_Semacquire 陷入睡眠状态。

1.3 例子

func TestMultiRoutineWaitGroup(t *testing.T) {
    wg := sync.WaitGroup{}
    wgCount := 10
    wg.Add(wgCount)
    for i := 0; i < wgCount; i++ {
        go func(i int) {
            fmt.Println(i, " in processing")
            wg.Done()
            time.Sleep(time.Millisecond * 1000)
            // 在子routine 中也等待所有的routine结束才执行下面的输出。
            wg.Wait()
            fmt.Println("all done, ", i, " ended!")
        }(i)
    }
    wg.Wait()
    time.Sleep(time.Second)
    fmt.Println("all done")
}

结果:

9  in processing
0  in processing
4  in processing
1  in processing
2  in processing
3  in processing
5  in processing
8  in processing
7  in processing
6  in processing
all done,  2  ended!
all done
all done,  7  ended!
all done,  8  ended!
all done,  0  ended!
all done,  3  ended!
all done,  9  ended!
all done,  5  ended!
all done,  4  ended!
all done,  1  ended!
all done,  6  ended!

2 Once

保证在 Go 程序运行期间 Once 对应的某段代码只会执行一次。

2.1 结构

type Once struct {
    m    Mutex
    done uint32
}

其结构里面包含了一个互斥锁。

2.2 流程

其核心流程就是一个Do函数,Do的操作就是先获取互斥锁,获取到了才执行。

func (o *Once) Do(f func()) {
    if atomic.LoadUint32(&o.done) == 1 {
        return
    }
    // Slow-path.
    o.m.Lock()
    defer o.m.Unlock()
    if o.done == 0 {
        defer atomic.StoreUint32(&o.done, 1)
        f()
    }
}

2.3 例子

func TestOnce(t *testing.T) {
    o := &sync.Once{}
    for i := 0; i < 10; i++ {
        go func(i int) {
            fmt.Println("routine: ", i)
            o.Do(func() {
                // 多试几次,发现并不是一定是第0个 routine 执行。
                fmt.Println(i , " dided")
            })
        }(i)
    }
}

结果:

routine:  1
1  dided
routine:  0
routine:  2
routine:  7
routine:  6
routine:  4
routine:  5
routine:  8
routine:  9
routine:  3

3 Cond

Cond 其实是一个条件变量,通过 Cond 可以让一系列的 Goroutine 都在触发某个事件或者条件时才被唤醒,每一个 Cond 结构体都包含一个互斥锁 L

3.1 结构

type Cond struct {
    noCopy noCopy

    // L is held while observing or changing the condition
    L Locker 

    notify  notifyList // 需要被通知的routine
    checker copyChecker
}

3.2 流程

Cond中,有一堆routine处于Wait状态,有些routine处于通知的状态。当处于Wait状态的routine收到通知才能继续执行。

3.2.1 Wait

Wait 方法会将当前 Goroutine 陷入休眠状态,它会先调用 runtime_notifyListAdd 将等待计数器 +1,然后解锁并调用 runtime_notifyListWait 等待其他 Goroutine 的唤醒

3.2.2 Broadcast

Broadcast 会唤醒队列中全部的 routine。

func (c *Cond) Broadcast() {
    c.checker.check()
    runtime_notifyListNotifyAll(&c.notify)
}
3.2.3 Signal

Signal 唤醒队列最前面的 routine。

func (c *Cond) Signal() {
    c.checker.check()
    runtime_notifyListNotifyOne(&c.notify)
}

3.3 例子

func TestCond(t *testing.T) {
    c := sync.NewCond(&sync.Mutex{})
    for i := 0; i < 10; i++ {
        go listen(c, i)
    }
    time.Sleep(1*time.Second)

    // 唤醒等待队列第一个routine ,等待最久的routine
    go signal(c)

    // 唤醒所有
    go broadcast(c)

    time.Sleep(time.Second * 5)

}

func signal(c *sync.Cond) {
    c.L.Lock()
    fmt.Println("before signal===========")
    // 仅仅会唤醒休眠队列前面的routine
    c.Signal()
    fmt.Println("after signal===========")
    c.L.Unlock()
}

func broadcast(c *sync.Cond) {
    c.L.Lock()
    fmt.Println("before broadcast===========")
    // 给所有wait的routine发送信号
    c.Broadcast()
    fmt.Println("after broadcast===========")

    c.L.Unlock()
}

func listen(c *sync.Cond, i int) {
    c.L.Lock()
    // 等待信号
    c.Wait()
    fmt.Println( "listen: ", i)
    c.L.Unlock()
    fmt.Println("after listen: ", i)
}

结果:

before signal===========
after signal===========
before broadcast===========
after broadcast===========
listen:  7
after listen:  7
listen:  9
after listen:  9
listen:  8
after listen:  8
listen:  3
after listen:  3
listen:  4
after listen:  4
listen:  6
after listen:  6
listen:  5
after listen:  5
listen:  2
after listen:  2
listen:  0
after listen:  0
listen:  1
after listen:  1

4 总结

本文是《同步原语与锁》的第二部分-WaitGroup, Once, Cond。从结构、流程、demo 三个维度进行了分析,希望对你有所帮助~

5 参考文献

同步原语与锁 https://draveness.me/golang/concurrency/golang-sync-primitives.html
Go 1.8 源码

6 其他

本文是《循序渐进go语言》的第十四篇-《Go-同步原语与锁(二)WaitGroup、Once与Cond》。
如果有疑问,可以直接留言,也可以关注公众号 “链人成长chainerup” 提问留言,或者加入知识星球“链人成长” 与我深度链接~

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,258评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,335评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,225评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,126评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,140评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,098评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,018评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,857评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,298评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,518评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,678评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,400评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,993评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,638评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,801评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,661评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,558评论 2 352

推荐阅读更多精彩内容