同步与锁
在并发条件下,信息的同步要求用户必须要用锁的机制来保证准确性。

互斥锁

Go 中的 sync 包提供了两种锁类型: sync.Mutexsync.RWMutex,前者是互斥锁,后者是读写锁。
互斥锁是传统的开发程序对共享资源进行访问控制的主要手段。在 Go 中更推崇使用通道来实现资源共享和通信。互斥锁由标准库 sync 包中的 Mutex 结构体类型实现,只有两个公共方法:调用 Lock() 获得锁和调用 Unlock() 释放锁。

  • 同一个协程中同步调用使用 Lock() 加锁后,不能再对其加锁,否则会引发运行时异常。只能在 Unlock() 之后再次 Lock()。多个协程中异步调用 Lock() 没有问题,但是每个协程只能调用一次 Lock()。由于多个协程之间产生了锁竞争,因此不会有运行时异常。互斥锁适用于只允许一个读或写的场景,所以该锁也叫作全局锁。
  • Unlock() 用于解锁,如果在使用 Unlock() 前未加锁,就会引起一个运行错误。已经锁定的 Mutex 并不与特定的协程相关,这样可以利用一个协程对其加锁,再利用其他协程对其解锁。

读写锁

读写锁是多读单写互斥锁,分别针对读操作和写操作进行锁定和解锁操作,经常用于读次数远远多于写次数的场合。读写锁由结构体类型 sync,RWMutex 实现。

  • 读锁定情况下,对读写锁进行读锁定或者写锁定,都将阻塞,而且读锁与写锁之间是互斥的。
  • 读锁定情况下,对读写锁进行写锁定,将阻塞;加读锁时不会阻塞,即可多读。
  • 对未被写锁定的读写锁进行写解锁,会引发运行时异常。
  • 对未被读锁定的读写锁进行读解锁时也会引发运行时异常。
  • 写解锁在进行的同时会试图唤醒所有因进行而被阻塞的协程。
  • 读解锁在进行的时候则会试图唤醒一个因进行写锁定而被阻塞的协程。

sync.WaitGroup

WaitGroup 用于线程总同步。它等待一组线程集合完成,才会继续向下执行。主线程调用 Add() 方法来设置等待的协程数量。然后每个协程运行,并在完成后调用 Done() 方法。同时,Wait() 方法用来阻塞主线程,直到所有线程完成才会向下执行。Add(-1) 和 Done() 效果一致,都表示等待的协程数量减少一个。

sync.Once

sync.Once.Do(f func()) 能保证 Do() 方法只执行一次。对只需要运行一次的代码,如全局性初始化操作,或者防止多次重复执行都有很好的作用。

sync.Map

sync.Map,它原生支持并发安全的字典。原有普通字典并不线程安全,一般情况下还以可以继续使用它。只有在涉及线程安全时才考虑 sync.Map,而且 sync.Map 的使用和字典有较大差异。

package main

import (
   . "fmt"
   "sync"
)

func main() {
   var m sync.Map
   // Stroe 保存数据
   m.Store("name", "Joe")
   m.Store("gender", "Male")

   // LoadOrStore
   // 若 key 不存在,则存入 key 和 value,返回 false 和输入的 value
   v, ok := m.LoadOrStore("name1", "Joe")
   Println(v, ok)

   // 若 key 已存在,则返回 true 和 key 对应的 value,不会修改原来的 value
   v, ok = m.LoadOrStore("name", "aaa")
   Println(v, ok)
   
   // Load 读取数据
   v, ok = m.Load("name")
   if ok {
       Println("key 存在, 值是: ", v)
   } else {
       Println("key 不存在")
   }
   // Range 遍历
   f := func(k, v interface{}) bool {
       Println(k, v)
       return true
   }
   m.Range(f)

   // Delete 删除 key 及数据
   m.Delete("name")
   Println(m.Load("name"))
}

程序输出

Joe false
Joe true
key 存在, 值是:  Joe
name Joe
gender Male
name1 Joe
<nil> false

sync.Cond

sync.Cond 基于互斥锁/读写锁。互斥锁 sync.Mutex 通常用来保护临界区和共享资源,条件变量 sync.Cond 用来协调想要访问共享资源的 goroutine
sync.Cond 经常用在多个 goroutine 等待,一个 goroutine 通知(事件发生)的场景。如果是一个通知,一个等待,使用互斥锁或 channle 就能解决。
我们想象一个非常简单的场景:
有一个协程在异步地接收数据,剩下的多个协程必须等待这个协程接收完数据,才能读取到正确的数据。在这种情况下,如果单纯使用 channle 或互斥锁,那么只能有一个协程可以等待,并读取到数据,没办法通知其他的协程也读取数据。

这个时候,就需要有个全局的变量来标志第一个协程数据是否接受完毕,剩下的协程,反复简单该变量的值,直到满足要求。或者创建多个 channel,每个协程阻塞在一个 channel 上,由接收数据的协程在数据接收完毕后,逐个通知。总之,需要额外的复杂度来完成这件事。

Go 语言在标准库 sync 中内置一个 sync.Cond 用来解决这类问题。