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” 提问留言,或者加入知识星球“链人成长” 与我深度链接~

你可能感兴趣的:(Go-同步原语与锁(二)WaitGroup、Once与Cond)