Go语言基础之并发安全和锁 | 青训营笔记

44 阅读2分钟

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

数据竞争

数据竞争:当两个或多个协程同时访问同一个内存地址,并且至少有一个是在写时,就会发生数据竞争

下面的实例表现了什么是数据竞争:

package main

import (
   "fmt"
   "sync"
)

//多个goroutine并发操作全局变量x

var (
   x    int64
   wg   sync.WaitGroup
   lock sync.Mutex
)

func add() {
   for i := 0; i < 5000; i++ {
      x = x + 1
   }
   wg.Done()
}

func main() {
   wg.Add(2)
   go add()
   go add()
   wg.Wait()
   fmt.Println(x)
}

输出结果:

image.png 在上述程序中开启了两个goroutine对全局变量x进行+1操作,两个 goroutine 在访问和修改全局的x变量时就会存在数据竞争,某个 goroutine 中对全局变量x的修改可能会覆盖掉另一个 goroutine 中的操作,所以导致最后的结果与预期不符。

互斥锁

互斥锁是一种常用的控制共享资源访问的方法,它能够保证同一时间只有一个 goroutine 可以访问共享资源。Go 语言中使用sync包中提供的Mutex类型来实现互斥锁。

sync.Mutex提供了两个方法供我们使用。

方法名功能
func (m *Mutex) Lock()获取互斥锁
func (m *Mutex) Unlock()释放互斥锁

下面的代码使用互斥锁修复了上面代码的问题:

package main

import (
   "fmt"
   "sync"
)

//多个goroutine并发操作全局变量x

var (
   x    int64
   wg   sync.WaitGroup
   lock sync.Mutex
)

func add() {
   for i := 0; i < 5000; i++ {
      lock.Lock()
      x = x + 1
      lock.Unlock()
   }
   wg.Done()
}

func main() {
   wg.Add(2)
   go add()
   go add()
   wg.Wait()
   fmt.Println(x)
}

image.png 使用互斥锁能够保证同一时间有且只有一个 goroutine 进入临界区,其他的 goroutine 则在等待锁;当互斥锁释放后,等待的 goroutine 才可以获取锁进入临界区,多个 goroutine 同时等待一个锁时,唤醒的策略是随机的。

读写互斥锁

RWMutex 相对友好些,是经典的单写多读模型。在读锁占用的情况下,会阻止写,但不阻止读,也就是多个 goroutine 可同时获取读锁(调用 RLock() 方法;而写锁(调用 Lock() 方法)会阻止任何其他 goroutine(无论读和写)进来,整个锁相当于由该 goroutine 独占。从 RWMutex 的实现看,RWMutex 类型其实组合了 Mutex。 sync.RWMutex提供了以下5个方法。

方法名功能
func (rw *RWMutex) Lock()获取写锁
func (rw *RWMutex) Unlock()释放写锁
func (rw *RWMutex) RLock()获取读锁
func (rw *RWMutex) RUnlock()释放读锁
func (rw *RWMutex) RLocker() Locker返回一个实现Locker接口的读写锁