写在开头
非原创,知识搬运工/整合工,仅用于自己学习,本文是对锁的学习
往期回顾
- 基础章节
- GMP章节
- 并发编程
带着问题去阅读
- 介绍一下抢锁流程
- 什么是自旋锁,优缺点
- Go的锁是公平的吗
- 锁的两种模式,切换时机
- 新的G更有机会拿到锁吗
- 互斥锁和读写锁区别
- 读写锁如何做到读锁不互斥,读写互斥
- Mutex如何挂起协程G,如何唤醒
- GO的锁可以重复上锁或解锁吗
- 什么是double-check
- 什么是死锁
1.源码复习
锁的源码阅读
我们知道,在多线程的情况下,很容易发生数据竞争和内存重排等现象,这给我们的程序带来很多的不确定性,加锁能很好的防止这种现象。GO的锁分为互斥锁和读写锁,读写锁又在互斥锁的基础上使用装饰器模式进行包装。
1.1互斥锁
其中互斥锁的实现原理为
- 本质上是一个协程G给一把锁(Mutex结构体实例)的state状态字段设置为(利用atomic.compareAndSwapInt32函数)上锁mutexLocked(mutexLocked=1),改变成功则返回,失败说明该锁正被其他协程G占用,开始抢锁lockSlow(锁上并没有记录是哪个协程给他上的锁)
- 先尝试进行自旋获取锁(自旋就是当一个线程获取锁,该锁已经被占用,则等待一段时间后再次尝试,如此循环,CPU层面实现的),自旋锁会一直占用CPU核(线程不做切换),当自旋次数满后还没获取锁则进入下一步
自旋锁的优缺点
自旋锁尽可能的减少线程的阻塞,这对于锁的竞争不激烈,且占用锁时间非常短的代码块来说性能能大幅度的提升,因为自旋的消耗会小于线程阻塞挂起再唤醒的操作的消耗,这些操作会导致线程发生两次上下文切换!
但是如果锁的竞争激烈,或者持有锁的线程需要长时间占用锁执行同步块,这时候就不适合使用自旋锁了,因为自旋锁在获取锁前一直都是占用 cpu 做无用功,占着 XX 不 XX,同时有大量线程在竞争一个锁,会导致获取锁的时间很长,线程自旋的消耗大于线程阻塞挂起操作的消耗,其它需要 cpu 的线程又不能获取到 cpu,造成 cpu 的浪费。所以这种情况下我们要关闭自旋锁。
- 此时不再主动获取锁,进入等待队列并休眠,等待被信号唤醒再去抢锁
- 还抢不到就重新开始自旋再来一遍(实际上自旋次数已经满了,不需要再自旋了,只是再次进入等待队列,这里我们可以看出)
- 长时间等待(1ms)则进入饥饿模式,更有机会被抢到锁,饥饿模式的存在说明了GO的锁是不公平的
这里我们不难看出新来的G更容易抢到锁,因为自旋的机制,旧的G即使从队列中被唤醒,因为无法自旋,若不是饥饿模式需要重新休眠,更难抢到
1.2读写锁
读写锁RWMutex是写锁优先,即读锁可以重复加,但当读写锁之间互斥
从结构体成员上可以看出是对mutex的包装
type RWMutex struct {
w Mutex // held if there are pending writers
writerSem uint32 // semaphore for writers to wait for completing readers
readerSem uint32 // semaphore for readers to wait for completing writers
readerCount atomic.Int32 // number of pending readers
readerWait atomic.Int32 // number of departing readers
}
Q:如何实现读锁重复添加?
A:读锁加锁并不是操作内部互斥锁,是执行readerCount+1的操作
Q:如何实现加写锁的时候不能加读锁
A:RWMutex添加写锁时会将readerCount设置为负数,读锁加锁时发现为负数则陷入休眠,等待被唤醒(写锁完成后用循环唤醒全部的读锁)
Q:如何实现加读锁时无法加写锁
A:readerWait大于0表示还有读锁存在,写锁陷入休眠,只有当readerWait==0,写锁才能开始加锁
1.3atomic
另一个同步原语atomic实现都是依赖底层汇编
2.实验-锁的错误使用
2.1同协程重复加互斥锁
type MyData struct {
id int
mu sync.Mutex
}
func TestLockAgain(t *testing.T) {
data := MyData{id: 1}
data.mu.Lock()
data.mu.Lock()
data.mu.Unlock()
data.mu.Unlock()
}
//panic: test timed out after 30s
运行超时引发的panic,这个就不分析了
2.2重复解锁
func TestUnLockAgain(t *testing.T) {
data := MyData{id: 0}
data.mu.Lock()
defer data.mu.Unlock()
data.mu.Unlock()
}
//fatal error: sync: unlock of unlocked mutex
造成原因通过阅读源码unlock的unlockslow函数就知道了,unlock尝试对Mutex的state字段-1,当值不为0则进入unlockslow,由unlockslow判断
上面的错误由下面代码引发
//上面情况new = -1 mutexLocked =1
func (m *Mutex) unlockSlow(new int32) {
if (new+mutexLocked)&mutexLocked == 0 {
fatal("sync: unlock of unlocked mutex")
}
2.3复制锁引发的错误
使用复制锁引发的错误归根揭底是无法确定这把锁的状态,因为锁上不保存上锁协程的信息,意味着G1上锁了,G2却能解锁,下面是给复制锁解锁的例子
func TestCopyMutex(t *testing.T) {
data := MyData{id: 0}
go func() {
data.mu.Lock()
defer data.mu.Unlock()
time.Sleep(time.Hour)
}()
time.Sleep(time.Second)
go func(mydata MyData) {
mydata.mu.Unlock()
mydata.id = 2
t.Log(mydata.id)
time.Sleep(time.Hour)
}(data)
time.Sleep(time.Second * 5)
}
此时代码没问题,锁被复制前是上锁的状态,若去掉第一个G中的defer关键词,则锁被复制时是解锁状态,重复解锁引发panic
所以上锁解锁的原则是在同一个函数体或大括号中
2.4死锁引发的panic
源码阅读那一章举过例子
func TestDeadlock(t *testing.T) {
d1 := MyData{id: 1}
d2 := MyData{id: 2}
go func() {
d1.mu.Lock()
defer d1.mu.Unlock()
time.Sleep(time.Second)
t.Log("g1-d1.id:", d1.id)
d2.mu.Lock()
t.Log("g1-d2.id:", d2.id)
d2.mu.Unlock()
}()
go func() {
d2.mu.Lock()
defer d2.mu.Unlock()
time.Sleep(time.Second)
t.Log("g2-d2.id:", d2.id)
d1.mu.Lock()
t.Log("g2-d1.id:", d1.id)
d1.mu.Unlock()
}()
var ch = make(chan int)
<-ch
}
G1和G2互相需要对面的资源和拥有的锁,都在互相等待对面释放锁,结果一起休眠,解决办法就是缩小锁住的范围或使用atomic
go func() {
d1.mu.Lock()
time.Sleep(time.Second)
t.Log("g1-d1.id:", d1.id)
d1.mu.Unlock() //使用完就释放,不再锁住整个函数
d2.mu.Lock()
t.Log("g1-d2.id:", d2.id)
d2.mu.Unlock()
}()
2.5双重检查-double-check
解决一个问题就会引发新的问题,如上,锁的粒度太细,在多线程下也会产生奇怪的结果
func TestDoubleCheck(t *testing.T){
type safeData struct{
values map[int]int
lock sync.RWMutex
}
data:=safeData{values:make(map[int]int)}
chang := func(key int,val int)int{
data.lock.RLock()
old,ok := data.values[key] //a1
data.lock.RUnlock()
if ok {
return old
}
data.lock.Lock()
defer data.lock.Unlock()
data.values[key] = val //a2
return val
}
go chang(0,1)
go chang(0,2)
}
G1和G2多核下同时执行:
- 读锁互不冲突,g1.a1和g2.a1互不冲突,都检测到未设置,要进行a2的操作
- 这里问题来了,这里写锁互相冲突,假设g1.a2先执行,已经完成赋值 value[0]=1,这时g2抢到锁后设置values[0]=2是不是不符合代码逻辑?因为values[0]已经赋值过了呀
为了保证上面的先行发生关系,有两种方法:
1.提升锁为互斥锁sync.mutex
2.使用双重检查,即加写锁后再读一次
chang2 := func(key int,val int)int{
data.lock.RLock()
old,ok := data.values[key]
data.lock.RUnlock()
if ok {
return old
}
data.lock.Lock()
defer data.lock.Unlock()
old,ok = data.values[key]
if ok {
return old
}
data.values[key] = val
return val
}
使用原则小结
- 适当减少锁的粒度和范围大小
- 尽早释放锁
- 避免复制锁
- 多线程环境下,注意双重检查
- 写多的操作使用Mutex
网友评论