Go语言sync包 | 青训营笔记

74 阅读3分钟

这是我参与「第五届青训营」笔记创作活动的第 15 天

sync.Mutex

sync.Mutex可能是sync包中使用最广泛的原语。它允许在共享资源上互斥访问(不能同时访问):

mutex := &sync.Mutex{}

mutex.Lock()
// Update共享变量 (比如切片,结构体指针等)
mutex.Unlock()

必须指出的是,在第一次被使用后,不能再对sync.Mutex进行复制。(sync包的所有原语都一样)。如果结构体具有同步原语字段,则必须通过指针传递它。

sync.RWMutex

sync.RWMutex是一个读写互斥锁,它提供了我们上面的刚刚看到的sync.MutexLockUnLock方法(因为这两个结构都实现了sync.Locker接口)。但是,它还允许使用RLockRUnlock方法进行并发读取:

mutex := &sync.RWMutex{}

mutex.Lock()
// Update 共享变量
mutex.Unlock()

mutex.RLock()
// Read 共享变量
mutex.RUnlock()

sync.RWMutex允许至少一个读锁或一个写锁存在,而sync.Mutex允许一个读锁或一个写锁存在。

通过基准测试来比较这几个方法的性能:

BenchmarkMutexLock-4       83497579         17.7 ns/op
BenchmarkRWMutexLock-4     35286374         44.3 ns/op
BenchmarkRWMutexRLock-4    89403342         15.3 ns/op

可以看到锁定/解锁sync.RWMutex读锁的速度比锁定/解锁sync.Mutex更快,另一方面,在sync.RWMutex上调用Lock()Unlock()是最慢的操作。

因此,只有在频繁读取和不频繁写入的场景里,才应该使用sync.RWMutex

sync.WaitGroup

sync.WaitGroup也是一个经常会用到的同步原语,它的使用场景是在一个goroutine等待一组goroutine执行完成。

sync.WaitGroup拥有一个内部计数器。当计数器等于0时,则Wait()方法会立即返回。否则它将阻塞执行Wait()方法的goroutine直到计数器等于0时为止。

要增加计数器,我们必须使用Add(int)方法。要减少它,我们可以使用Done()(将计数器减1),也可以传递负数给Add方法把计数器减少指定大小,Done()方法底层就是通过Add(-1)实现的。

在以下示例中,我们将启动八个goroutine,并等待他们完成:

wg := &sync.WaitGroup{}

for i := 0; i < 8; i++ {
  wg.Add(1)
  go func() {
    // Do something
    wg.Done()
  }()
}

wg.Wait()
// 继续往下执行...

每次创建goroutine时,我们都会使用wg.Add(1)来增加wg的内部计数器。我们也可以在for循环之前调用wg.Add(8)

与此同时,每个goroutine完成时,都会使用wg.Done()减少wg的内部计数器。

main goroutine会在八个goroutine都执行wg.Done()将计数器变为0后才能继续执行。

sync.Map

sync.Map是一个并发版本的Go语言的map,我们可以:

  • 使用Store(interface {},interface {})添加元素。
  • 使用Load(interface {}) interface {}检索元素。
  • 使用Delete(interface {})删除元素。
  • 使用LoadOrStore(interface {},interface {}) (interface {},bool)检索或添加之前不存在的元素。如果键之前在map中存在,则返回的布尔值为true
  • 使用Range遍历元素。
m := &sync.Map{}

// 添加元素
m.Store(1, "one")
m.Store(2, "two")

// 获取元素1
value, contains := m.Load(1)
if contains {
  fmt.Printf("%s\n", value.(string))
}

// 返回已存value,否则把指定的键值存储到map中
value, loaded := m.LoadOrStore(3, "three")
if !loaded {
  fmt.Printf("%s\n", value.(string))
}

m.Delete(3)

// 迭代所有元素
m.Range(func(key, value interface{}) bool {
  fmt.Printf("%d: %s\n", key.(int), value.(string))
  return true
})

上面的程序会输出:

one
three
1: one
2: two

如你所见,Range方法接收一个类型为func(key,value interface {})bool的函数参数。如果函数返回了false,则停止迭代。有趣的事实是,即使我们在恒定时间后返回false,最坏情况下的时间复杂度仍为O(n)

我们应该在什么时候使用sync.Map而不是在普通的map上使用sync.Mutex

  • 当我们对map有频繁的读取和不频繁的写入时。
  • 当多个goroutine读取,写入和覆盖不相交的键时。具体是什么意思呢?例如,如果我们有一个分片实现,其中包含一组4个goroutine,每个goroutine负责25%的键(每个负责的键不冲突)。在这种情况下,sync.Map是首选。