go语言之RWMutex

RWMutex特点

读写锁区别与互斥锁的主要区别就是读锁之间是共享的,多个goroutine可以同时加读锁,但是写锁与写锁、写锁与读锁之间则是互斥的。
因为读锁是共享的,所以如果当前已经有读锁,那后续goroutine继续加读锁正常情况下是可以加锁成功,但是如果一直有读锁进行加锁,那尝试加写锁的goroutine则可能会长期获取不到锁,这就是因为读锁而导致的写锁饥饿问题。如何解决写锁饥饿问题?

RWMutex的数据结构

type RWMutex struct {
    w           Mutex  // 互斥锁
    writerSem   uint32 // 用于writer等待读完成排队的信号量
    readerSem   uint32 // 用于reader等待写完成排队的信号量
    readerCount int32  // 读锁的计数器
    readerWait  int32  // 等待读锁释放的数量
}

const rwmutexMaxReaders = 1 << 30   // 支持最多2^30个读锁

在go里面对写锁的计数采用了负值进行,通过递减最大允许加读锁的数量从而进行写锁对读锁的抢占。

读锁实现

RLock加读锁实现

func (rw *RWMutex) RLock() {
	if race.Enabled {
		_ = rw.w.state
		race.Disable()
	}
	// 累加reader计数器,累加结果如果小于0则表明有writer正在等待
	// 每次 goroutine获取读锁时:readCount+1
    // 如果写锁已经被获取,那么 readCount 在 -rwmutexMaxReaders 与 0 之间(当为0的时候,代表有2^30个读锁在等待,应该会出错,但是极端条件不会出现)
   // 通过readCount 判断读锁与写锁是否互斥,如果有写锁存在就挂起 goroutine,多个读锁可以并行
	if atomic.AddInt32(&rw.readerCount, 1) < 0 {
		 // 当前有writer正在等待读锁,等待读信号量唤醒,唤醒的时机:写锁释放的时候
		runtime_SemacquireMutex(&rw.readerSem, false, 0)
	}
	if race.Enabled {
		race.Enable()
		race.Acquire(unsafe.Pointer(&rw.readerSem))
	}
}

RUnlock释放读锁实现

func (rw *RWMutex) RUnlock() {
	if race.Enabled {
		_ = rw.w.state
		race.ReleaseMerge(unsafe.Pointer(&rw.writerSem))
		race.Disable()
	}
	// 检查当前是否可以进行释放锁
	// 如果小于0,则表明当前有writer正在等待
	if r := atomic.AddInt32(&rw.readerCount, -1); r < 0 {
		// 1.r+1==0时,rw.readerCount -1= -1,rw.readerCount  = 0则不存在读锁,表示直接执行RUnlock()
       // 2.r+1=-rwmutexMaxReaders,rw.readerCount = -rwmutexMaxReaders ,
       // 这种情况出现在获取Lock()方法,atomic.AddInt32(&rw.readerCount, -rwmutexMaxReaders),这时rw.readerCount  = 0 也不存在读锁,表示执行Lock()再执行RUnlock()
		if r+1 == 0 || r+1 == -rwmutexMaxReaders { //如果已经没有读锁的,还去释放(如释放多次)
			race.Enable()
			throw("sync: RUnlock of unlocked RWMutex")
		}
		// 全部读锁释放完毕后,释放写信号量
		if atomic.AddInt32(&rw.readerWait, -1) == 0 {
			// 释放写信号量
			runtime_Semrelease(&rw.writerSem, false, 1) 
		}
	}
	if race.Enabled {
		race.Enable()
	}
}

Lock写锁加锁实现

func (rw *RWMutex) Lock() {
	if race.Enabled {
		_ = rw.w.state
		race.Disable()
	}
	// 保证写锁唯一
	rw.w.Lock()
	//  对readerCounter进行进行抢占,通过递减rwmutexMaxReaders允许最大读的数量
	// 来实现写锁对读锁的抢占
	r := atomic.AddInt32(&rw.readerCount, -rwmutexMaxReaders) + rwmutexMaxReaders
	// 若有读锁,则等待获取写信号来。写信号量什么时候释放:全部读锁释放完毕
	if r != 0 && atomic.AddInt32(&rw.readerWait, r) != 0 {
		runtime_SemacquireMutex(&rw.writerSem, false, 0)
	}
	if race.Enabled {
		race.Enable()
		race.Acquire(unsafe.Pointer(&rw.readerSem))
		race.Acquire(unsafe.Pointer(&rw.writerSem))
	}
}

Unlock写锁释放实现

func (rw *RWMutex) Unlock() {
	if race.Enabled {
		_ = rw.w.state
		race.Release(unsafe.Pointer(&rw.readerSem))
		race.Disable()
	}
	// 将reader计数器复位,上面减去了一个rwmutexMaxReaders现在再重新加回去即可复位
	r := atomic.AddInt32(&rw.readerCount, rwmutexMaxReaders)
	if r >= rwmutexMaxReaders {
		race.Enable()
		throw("sync: Unlock of unlocked RWMutex")
	}
	// 唤醒所有的读锁
	for i := 0; i < int(r); i++ {
		// 唤醒所有读信号量
		runtime_Semrelease(&rw.readerSem, false, 0)
	}
	// 释放mutex
	rw.w.Unlock()
	if race.Enabled {
		race.Enable()
	}
}

关键核心机制

写锁对读锁的抢占

1. 加写锁的抢占
在加写锁的的时候,更改rw.readerCount的值(加写锁的抢占)
r := atomic.AddInt32(&rw.readerCount, -rwmutexMaxReaders) + rwmutexMaxReaders
2. 加读锁的时候检测是否加了写锁
同时在加读锁的时候,判断rw.readerCount的值(读锁对写锁的抢占检测)
if atomic.AddInt32(&rw.readerCount, 1) < 0 {
	 // 当前有writer正在等待读锁,等待读信号量唤醒,唤醒的时机:写锁释放的时候
	runtime_SemacquireMutex(&rw.readerSem, false, 0)
}
3. 加写锁的时候,发现前面还有写锁,则等待前面的读锁全部释放完毕
if r != 0 && atomic.AddInt32(&rw.readerWait, r) != 0 {
    // 写锁发现需要等待的读锁释放的数量不为0,就自己自己去休眠了
    runtime_SemacquireMutex(&rw.writerSem, false)
}
4. 写锁休眠。必定什么时候可以进行唤醒写锁——等待前面的所有读锁都释放,即读锁释放的代码实现
if atomic.AddInt32(&rw.readerWait, -1) == 0 {
    // The last reader unblocks the writer.
    runtime_Semrelease(&rw.writerSem, false)
}

写锁的公平性

在加写锁的时候必须先进行mutex的加锁,而mutex本身在普通模式下是非公平的,只有在饥饿模式下才是公平的。rw.w.Lock()

读锁与写锁的公平性

在加读锁和写锁的工程中都使用atomic.AddInt32来进行递增,而该指令在底层是会通过LOCK来进行CPU总线加锁的,因此多个CPU同时执行readerCount其实只会有一个成功,从这上面看其实是写锁与读锁之间是相对公平的,谁先达到谁先被CPU调度执行,进行LOCK锁cache line成功,谁就加成功锁

可见性与原子性

在并发场景中特别是JAVA中通常会提到并发里面的两个问题:可见性与内存屏障、原子性, 其中可见性通常是指在cpu多级缓存下如何保证缓存的一致性,即在一个CPU上修改了了某个数据在其他的CPU上不会继续读取旧的数据,内存屏障通常是为了CPU为了提高流水线性能,而对指令进行重排序而来,而原子性则是指的执行某个操作的过程的不可分割

go里面并没有volatile这种关键字,那如何能保证上面的AddInt32这个操作可以满足上面的两个问题呢, 其实关键就在于底层的2条指令,通过LOCK指令配合CPU的MESI协议,实现可见性和内存屏障,同时通过XADDL则用来保证原子性,从而解决上面提到的可见性与原子性问题

// atomic/asm_amd64.s TEXT runtime∕internal∕atomic·Xadd(SB)
LOCK
XADDL    AX, 0(BX)

参考文献

图解golang里面的读写锁实现与核心原理分析了解编程语言背后设计
sync包 rwmutex源码阅读

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章