Go并发编程(五)同步锁Mutex&读写锁RWMutex
原创
©著作权归作者所有:来自51CTO博客作者wx5add7776993de的原创作品,请联系作者获取转载授权,否则将追究法律责任
文章目录
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)
}()
}
}
原理
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 // 锁模式切换的阈值
)
加锁流程:
- 先通过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的RWMutex是写优先
数据结构定义:
type RWMutex struct {
w Mutex // 互斥锁解决多个writer的竞争
writerSem uint32 // writer信号量
readerSem uint32 // reader信号量
readerCount int32 // reader的数量
readerWait int32 // writer等待完成的reader的数量
}