文章目录

Go并发编程(五)同步锁Mutex&读写锁RWMutex

Mutex

使用

Mutex是go中实现的同步锁,保证了同一时间只有一个goroutine执行

func TestLock(mutex *sync.Mutex)  {
fmt.Println("主协程 start")
time.Sleep(2 * time.Second)
for i := 0;i < 10;i++{
// 开启10个协程
time.Sleep(1 * time.Second)
go func() {
mutex.Lock()
defer mutex.Unlock()
fmt.Println("第 ",i,"个协程")
time.Sleep(time.Second * 2)
}()
}
}

Go并发编程(五)同步锁Mutex&读写锁RWMutex_同步锁

原理

Mutex的相关数据结构:

type Mutex struct {
state int32 // 互斥锁状态
sema uint32 // 用来控制等待 goroutine 的阻塞休眠和唤醒
}

饥饿问题:

  • 在某个请求中,某些协程可能长时间获取不到锁,导致业务逻辑不能完整执行,而当前正在CPU上执行的协程可能会更容易获取到锁

正常模式&饥饿模式:

  • 正常模式:所有的goroutine按照先进先出顺序排队等待锁,被唤醒的goroutine和新请求锁的goroutine同时请求锁,新请求锁的goroutine更容易获取锁,被唤醒的goroutine不容易获取到锁
  • 饥饿模式:所有goroutine排队等待锁,新请求的goroutine不会进行获取锁,而是排到队尾等待

新请求获取锁的goroutine更容易获取锁,why?

官方解释是说新请求获取锁的goroutine正在CPU上执行,更具优势,而被唤醒的goroutine则很大可能在获取锁竞争中失败

锁状态status详解:

const (
mutexLocked = 1 << iota // 锁状态
mutexWoken // 唤醒状态
mutexStarving // 锁模式
mutexWaiterShift = iota // 等待锁的goroutine数量
starvationThresholdNs = 1e6 // 锁模式切换的阈值
)

Go并发编程(五)同步锁Mutex&读写锁RWMutex_同步锁_02

加锁流程:

  1. 先通过CAS修改锁状态变量,CAS修改成功,则成功加锁,失败则进入自旋加锁流程
func (m *Mutex) Lock() {
// Fast path: grab unlocked mutex.
// CAS加锁
if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {
if race.Enabled {
race.Acquire(unsafe.Pointer(m))
}
return
}
// 失败则自旋加锁
// Slow path (outlined so that the fast path can be inlined)
m.lockSlow()
}

RWMutex

RWMutex是一个读写锁,该锁可以加多个读锁或者一个写锁,其经常用于读次数远远多于写次数的场景(如果读写相当,用Mutex没啥区别)。但加读锁时,只能继续加读锁;当有写锁时,无法加载其他任何锁。也就是说,只有读-读之间是共享的,其它为互斥的。

使用

func TestRWLock() {
wg := sync.WaitGroup{}
wg.Add(20)
var rwMutex sync.RWMutex
Data := 0
for i := 0; i < 10; i++ {
go func(t int) {
rwMutex.RLock()
defer rwMutex.RUnlock()
fmt.Printf("Read data: %v\n", Data)
wg.Done()
time.Sleep(2 * time.Second)
// 这句代码第一次运行后,读解锁。
// 循环到第二个时,读锁定后,这个goroutine就没有阻塞,同时读成功。
}(i)
go func(t int) {
rwMutex.Lock()
defer rwMutex.Unlock()
Data += t
fmt.Printf("Write Data: %v %d \n", Data, t)
wg.Done()
// 这句代码让写锁的效果显示出来,写锁定下是需要解锁后才能写的。
time.Sleep(2 * time.Second)
}(i)
}
time.Sleep(5 * time.Second)
wg.Wait()
}

Go并发编程(五)同步锁Mutex&读写锁RWMutex_同步锁_03

原理

读——写问题可以分为三类:

  • 读优先。读进程占有锁时,后来的读进程可以立即获得锁。这样做的好处是可以提高并发性能(后来的读进程不需要等待),坏处是如果读进程过多,会导致写进程一直处于等待中,出现写饥饿现象。
  • 写优先。写优先是指如果有写进程在等待锁,会阻止后来的读进程获得锁(当然也会阻塞写进程)。写优先保证的是新来的进程,这样就避免了写饥饿的问题。
  • 不区分优先级。不区分优先级。这个其实就是正常互斥锁的逻辑。

Go的RWMutex是写优先

数据结构定义:

type RWMutex struct {
w Mutex // 互斥锁解决多个writer的竞争
writerSem uint32 // writer信号量
readerSem uint32 // reader信号量
readerCount int32 // reader的数量
readerWait int32 // writer等待完成的reader的数量
}