一文看懂golang的sync包

一条大菜狗HS · · 621 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

一文看懂golang的sync包

sync包里包含的struct以及其功能

  • sync.Mutex: 互斥量(锁),主要是处理多个goroutine竞争同一个资源时候的同步问题。
  • sync.RWMutex: 读写互斥量(锁),相对Mutex而言能进行更细腻的控制,主要用在读多写少的情景下。
  • sync.WaitGroup: WaitGroup用于等待一组goroutine结束。
  • sync.Cond:实现一个条件变量,即等待或宣布事件发生的goroutines的会合点。
  • sync.Pool:临时对象池,作为临时对象的保存和复用的集合。
  • sync.Once:顾名思义,Once可以使得函数的调用只执行一次。

具体解释和示例

  • Mutex 主要处理多个goroutine竞争同一个资源时的同步问题,能保证同时只有一个goroutine在使用该资源,而其他的goroutine则在等待,直到占用资源的goroutine释放了Mutex,即是调用了mutex.Unlock()
    示例:
package main

import (
    "fmt"
    "sync"
    "time"
)

func main() {
    ch := make(chan struct{}, 2)

    var l sync.Mutex
    go func() {
        l.Lock()
        defer l.Unlock()
        fmt.Println("goroutine1: lock 2 seconds")
        time.Sleep(time.Second * 2)
        fmt.Println("goroutine1: unlocked")
        ch <- struct{}{}
    }()

    go func() {
        fmt.Println("goroutine2: wait for unlock")
        l.Lock()
        defer l.Unlock()
        fmt.Println("goroutine2: lock 2 seconds")
        ch <- struct{}{}
    }()

    for i:=0;i<2;i++ {
        <- ch
    }
}

执行结果

$ go run main.go
goroutine2: wait for unlock
goroutine1: lock 2 seconds
goroutine1: unlocked
goroutine2: lock 2 seconds

从结果可以看到goroutine2在调用l.Lock()时阻塞了,一直等待goroutine1,直到在goroutine1里调用了l.Unlock()。

  • RWMutex 则可以对锁进行更精细的控制,主要的规则如下:
    • 读锁之间是不互斥的
    • 读锁和写锁互斥
    • 写锁和写锁互斥
      也就意味着同时只有一个goroutine能获得写锁定,当它获得了写锁定了,其他的goroutine,无论是写锁定还是读锁定,都无法获得。而可以同时有一个或多个goroutine获得读锁定,这时其他的goroutine无法获得写锁定。
      示例如下:
package main

import (
    "fmt"
    "math/rand"
    "sync"
    "time"
)

var count int
var rw sync.RWMutex

func main() {
    ch := make(chan struct{}, 10)
    for i:=0;i<5;i++ {
        go read(i, ch)
    }
    
    for i:=0;i<5;i++ {
        <- ch
    }
}

func read(i int, ch chan struct{}) {
    rw.RLock()
    fmt.Printf("goroutine %d 进入读操作\n", i)
    v := count
    time.Sleep(time.Second * 1)
    fmt.Printf("goroutine %d 读取结束,值为:%d\n", i, v)
    rw.RUnlock()
    ch <- struct{}{}
}

示例输出为:

goroutine 4 进入读操作
goroutine 1 进入读操作
goroutine 0 进入读操作
goroutine 2 进入读操作
goroutine 3 进入读操作
goroutine 1 读取结束,值为:0
goroutine 4 读取结束,值为:0
goroutine 3 读取结束,值为:0
goroutine 2 读取结束,值为:0
goroutine 0 读取结束,值为:0

可以看到5个goroutine可以同时进入读操作,而不会有任何的goroutine会阻塞。

对上面的例子稍作调整:

package main

import (
    "fmt"
    "math/rand"
    "sync"
    "time"
)

var count int
var rw sync.RWMutex

func main() {
    ch := make(chan struct{}, 10)
    for i:=0;i<5;i++ {
        go read(i, ch)
    }

    go write(ch)

    for i:=0;i<6;i++ {
        <- ch
    }
}

func read(i int, ch chan struct{}) {
    rw.RLock()
    fmt.Printf("goroutine %d 进入读操作\n", i)
    v := count
    time.Sleep(time.Second * 1)
    fmt.Printf("goroutine %d 读取结束,值为:%d\n", i, v)
    rw.RUnlock()
    ch <- struct{}{}
}

func write(ch chan struct{}) {
    rw.Lock()
    fmt.Printf("goroutine write 进入写操作\n")
    v := rand.Intn(1000)
    count = v
    fmt.Printf("goroutine write 写入结束,新值为:%d\n", count)
    rw.Unlock()
    ch <- struct{}{}
}

加入写锁,输出如下(因为多线程的原因,结果不唯一):

goroutine 1 进入读操作
goroutine 0 进入读操作
goroutine 1 读取结束,值为:0
goroutine 0 读取结束,值为:0
goroutine write 进入写操作
goroutine write 写入结束,新值为:81
goroutine 2 进入读操作
goroutine 3 进入读操作
goroutine 4 进入读操作
goroutine 2 读取结束,值为:81
goroutine 4 读取结束,值为:81
goroutine 3 读取结束,值为:81

可以看到加入了写锁之后,2,3,4这三个goroutine都必须等到goroutine write写入结束才能进入读操作。而0,1这两个goroutine这两个读操作完成之后,才能进入写goroutine。也就说明读写之间是互斥的,而多个读之间是不互斥的。

  • sync.WaitGroup 主要用于等待一组goroutine结束。一般用于控制主线程等待所有的goroutine都结束之后再结束。
    示例如下:
package main

import (
    "fmt"
    "sync"
    "time"
)

func main() {
    wg := sync.WaitGroup{}

    for i:=0;i<5;i++{
        wg.Add(1)
        go func(i int) {
            defer wg.Done()
            fmt.Printf("goroutine %d starts sleep\n", i)
            time.Sleep(time.Second * time.Duration(i))
            fmt.Printf("goroutine %d finished sleep\n", i)
        }(i)
    }
    fmt.Println("wait group waiting...")
    wg.Wait()
    fmt.Println("wait group waitint finished")
}

输出如下:

wait group waiting...
goroutine 1 starts sleep
goroutine 3 starts sleep
goroutine 4 starts sleep
goroutine 0 starts sleep
goroutine 0 finished sleep
goroutine 2 starts sleep
goroutine 1 finished sleep
goroutine 2 finished sleep
goroutine 3 finished sleep
goroutine 4 finished sleep
wait group waitint finished

可以看到wg.Wait()这一句阻塞住了主线程,直到所有的goroutine结束之后,这里才不再阻塞。

  • Once 相对而言是比较简单的,就是控制让某个函数仅仅执行一次,后面无论怎么调用,都不会再执行了。
    示例如下:
package main

import (
    "fmt"
    "sync"
)

func onceFunc(i int) {
    fmt.Printf("goroutine %d run", i)
}

func main() {
    ch := make(chan struct{}, 10)
    var once sync.Once

    for i:=0;i<10;i++ {
        go func() {
            once.Do(func() {
                onceFunc(i)
            })
            ch <- struct{}{}
        }()
    }

    for i:=0;i<10;i++ {
        <- ch
    }
}

输出如下(多线程的原因,输出结果不唯一):

goroutine 3 run

可以看到尽管我们在代码里调用了10次,但是实际上只会执行1次。

  • Cond 实现一个条件变量。代码示例如下:
package main

import (
    "fmt"
    "sync"
    "time"
)

var count int = 4

func main() {
    ch := make(chan struct{}, 5)

    var l sync.Mutex
    cond := sync.NewCond(&l)

    for i:=0;i<5;i++ {
        go func(i int) {
            cond.L.Lock()
            defer func() {
                cond.L.Unlock()
                ch <- struct{}{}
            }()
            for count > i {
                cond.Wait()
                fmt.Printf("收到一个通知 goroutine%d\n", i)
            }
            fmt.Printf("goroutine%d 执行结束\n", i)
        }(i)
    }

    time.Sleep(time.Millisecond * 20)
    fmt.Println("broadcast...")
    cond.L.Lock()
    count -= 1
    cond.Broadcast()
    cond.L.Unlock()

    time.Sleep(time.Second * 1)
    fmt.Println("signal...")
    cond.L.Lock()
    count -= 2
    cond.Signal()
    cond.L.Unlock()

    time.Sleep(time.Second)
    fmt.Println("broadcast...")
    cond.L.Lock()
    count -= 1
    cond.Broadcast()
    cond.L.Unlock()

    for i:=0; i<5;i++{
        <- ch
    }
}

输出:

goroutine4 执行结束
broadcast...
收到一个通知 goroutine0
收到一个通知 goroutine3
goroutine3 执行结束
收到一个通知 goroutine2
收到一个通知 goroutine1
signal...
收到一个通知 goroutine0
broadcast...
收到一个通知 goroutine0
goroutine0 执行结束
收到一个通知 goroutine2
goroutine2 执行结束
收到一个通知 goroutine1
goroutine1 执行结束

可以看到cond主要是通过条件(count > i)和cond.Wait()函数来阻塞goroutine,而在外部修改了count之后,通过外部的Signal和Broadcast函数,通知到Lock了这个cond的goroutine,当条件满足时就不再阻塞了,而如果条件不满足,继续阻塞。

  • Pool主要用于存临时对象,具体细节,且听下回分解。

有疑问加站长微信联系(非本文作者)

本文来自:简书

感谢作者:一条大菜狗HS

查看原文:一文看懂golang的sync包

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

621 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传