golang 中的锁机制

二维码
| Jul 20, 2019 | 原创

并发场景下避免不了锁的使用,Go 提供两种类型的锁:互斥锁读写锁 。下面分享下 Go 中锁的使用经验。

锁的声明

在使用一个锁之前需要声明一个锁,以下三种方式都可以表示声明了一个锁,即可立即使用,尤其是第一种方式,表示声明了一个初始化默认值为零的锁,官方给出的结论是,一个零值的锁表示的是解锁状态的锁:

The zero value for a Mutex is an unlocked mutex.

var mutexA sync.Mutex
var mutexB = &sync.Mutex{}
var mutexC = new(sync.Mutex)

// 使用锁
mutexA.Lock()

当然,如上第二种和第三种方式也可以使用 Go 中的 := 方式声明,显得更为简洁:

mutexB := &sync.Mutex{}
mutexC := new(sync.Mutex)

锁的拷贝

Go 官方文档中明确说明,一旦声明了一个锁,那么这个说之后不应出现拷贝复制情况,不然锁是不起作用的。简而言之就是:跨 goroutine 的多个锁场景操作应该使用同一把锁。

A Mutex must not be copied after first use. - https://golang.org/pkg/sync/#Mutex

正确方式:

var mutex sync.Mutex
go func() {
  mutex.Lock()
  // 并发操作
  mutex.Unlock()
}()

go func() {
  mutex.Lock()
  // 并发操作
  mutex.Unlock()
}()

错误方式:

go func() {
  var mutex sync.Mutex
  mutex.Lock()
  // 并发操作
  mutex.Unlock()
}()

go func() {
  var mutex sync.Mutex
  mutex.Lock()
  // 并发操作
  mutex.Unlock()
}()

各玩各的,达不到锁的目的。

读写锁

读写锁常用于:读多写少的场景。例如,多个 goroutine 同时并发读同一个文件,另有其他的 goroutine 同时会并发写同一个文件。

对于读写锁使用时需要注意的是: 对于读和写操作都需要加锁,如果只对写加锁,并未对读加锁,那么也是达不到预想效果的。加锁的具体实现就是,读加读锁,写加写锁即可。

var fileName = "/tmp/test.log"
var mutex sync.RWMutex

// 写文件
go func() {
  // 写锁
  mutex.Lock()
  _ = ioutil.WriteFile(fileName, []byte("writing log"), 0644)
  mutex.Unlock()
}()

// 并发读
for i := 0; i < 10; i++ {
  go func() {
    mutex.RLock()
    fileData, _ := ioutil.ReadFile(fileName)
    fmt.Println(fileData)
    mutex.Unlock()
  }()
}

读写锁的优势在于,多个读操作之间不互斥(不会阻塞),只有在读和写操作中为互斥 ,即:在有读写锁条件下,倘若当下有写操作,那么所有的读操作就会阻塞,直到写锁被释放;反之,如果有读操作,那么写操作需要等所有的读操作释放之后方可以写操作。