Go并发编程-Mutex
简单用
互斥锁Mutex提供了两个方法Lock和Unlock:进入临界区之前调用Lock方法,退出临界区的时候调用Unlock方法,当一个goroutine拿到了锁,就会阻塞其他gotoute在调用lock的方法上,直到这个锁被释放,并自己获取到了锁。
type Count struct {
sync.Mutex
Count int
}
func (c *Count) SyncAdd() {
c.Lock()
defer c.Unlock()
c.Count++
}
1. 使用Mutex是不需要初始化的,在使用时会自动去初始化。所以在使用时不需要自己去创建一个对象
2. Mutex变量声明时最好声明在其临界变量的上面,然后使用空格把字段分隔开来。逻辑会跟清晰,便于维护
3. Lock和Unlock方法成对出现,使用defer去解锁。逻辑会跟清晰,便于维护,防止出现漏掉的情况
看实现
type Mutex struct {
state int32 //标记锁的状态
sema uint32 //信号变量用来控制goroutine的阻塞休眠和唤醒
}
使用一个int32类型去标记锁的情况。并使用bit位去表示不同的含义。
const (
mutexLocked = 1 << iota // mutex is locked 已锁定
mutexWoken // 唤醒标记
mutexStarving //饥饿状态
mutexWaiterShift = iota //前三位是状态标记,后面的都是waiter的数量
第1位表示锁定状态,第二位表示唤醒标记,第三位表示饥饿模式,其余用来表示waiter等待的数量。
Mutex获取锁的过程经过三次的改版,最新的一版和目前的java中的锁的级别(偏向锁,轻量锁,重量锁)基本相匹配
第一次版本:是一个公平锁,所有的请求锁的goroutine都会在排队等待获取锁,这种锁性能不是很好,涉及到新来的goroutine与队列中的goroutine到底是应该获取锁,如果让新来的gorouine获取锁,不需要切换上下问,性能会比较好。
第二次版本:给新人机会(轻量锁),新来的goroutine与唤醒队列中的goroutine相比,更有机会获取到锁,甚至一个goroutine能连续获取到锁。
第三次版本:多给些机会(偏向锁),新来的goroutine或者是被唤醒的goroutine获取不到锁时,会通过自旋的方式去尝试再次获取锁,如果临界区代码执行非常短,这种是非常好的优化。抢夺锁的goroutine不用通过休眠唤醒方式等待调度,这样性能会比较好。
终极版本:解决饥饿(公平锁),由于新来的goroutine参与竞争锁,大概率是新来的goroutine获取锁,这种导致队列中的goroutine会一直获取不到锁,第四次版本让Mutex变得更加公平。引入了饥饿模式,在处于饥饿模式下,新来的goroutine是不参与竞争锁的,而是直接从队列中唤醒的goroutine去获得锁。正常模式和饥饿模式的转换的时机是队列中的goroutine超过1ms获取不到锁就进入到饥饿模式,否则就退出饥饿模式。
总结:最终版的Mutex与java锁定是非常类似的。在竞争锁时,最开始时是偏向锁级别,通过自旋的方式尝试获取锁,如果没有获取到就晋级为轻量级锁,新来的gorotutine与队列中的goroutine同时竞争,当队列中的goroutine超过1ms没有获取到锁则晋级为重量锁,队列中的goroutine优先获取到锁,不同的是java中不会降级,而Mutex是会降级的,会从饥饿模式退化为普通模式。也就是从重量锁退化为偏向锁
别踩坑
-
Lock与Unlock不是成对出现
不成对出现意味着死锁,或者未加锁时Unlock导致panic。死锁情况很好解释,锁一直处于锁定状态,意味着其他的goroutine永远没有机会获取到锁。未加锁的panic是终极版本的Mutex源码中有的,如果没有锁定直接Unlock是会直接panic的
func (m *Mutex) unlockSlow(new int32) { if (new+mutexLocked)&mutexLocked == 0 { throw("sync: unlock of unlocked mutex") } ... }
- Copy已使用的Mutex
sync的包中的同步原语是不能复制的,因为他们是有状态的对象,复制一个已经加锁的Mutex给一个新的变量明显不符合预期。虽然这种情况很简单,但是还是会很容易出错,因为go的函数调用会自动复制。所以有关锁的参数传递要使用指针的方式。
- 重入锁
java中的ReentrantLock就是重入锁,当一个线程获取到锁时,这个线程是可以无限的再次获取锁的,因为ReentrantLock记录了获取锁的线程的id。但是Mutex不是重入锁,他没有记录Goroutine的id。可以自己手动实现一个重入锁
package mutex import ( "fmt" "github.com/petermattis/goid" "sync" "sync/atomic" ) // RecursiveMutex 重入锁 type RecursiveMutex struct { sync.Mutex owner int64 // 当前持有锁的goroutine id recursion int32 // 重入的次数 } func (rm *RecursiveMutex) Lock() { gid := goid.Get() if atomic.LoadInt64(&rm.owner) == gid { rm.recursion++ return } rm.Mutex.Lock() atomic.StoreInt64(&rm.owner, gid) rm.recursion = 1 } func (rm *RecursiveMutex) Unlock() { gid := goid.Get() if atomic.LoadInt64(&rm.owner) != gid { panic(fmt.Sprintf("wrong the owner(%d) %d", rm.owner, gid)) } rm.recursion-- if rm.recursion != 0 { return } atomic.StoreInt64(&rm.owner, -1) rm.Mutex.Unlock() }
扩展用
Mutex本身只提供了Lock和Unlock的接口,但是在使用过程中会有一些特殊的场景需要特殊使用
-
Try lock功能
在Mutex在Luck时会直接进入到阻塞中,但是实际的应用中,当没有获取到锁时,并不希望阻塞住。这个时候就需要一个try Lock的功能,如果上锁成功就返回true,否则返回false
-
获取锁的信息
Mutex一个state字段包含了4层含义,锁的状态,清除标记,饥饿模式,以及waiter的数量,但是并没有获取到这个4个含义的值的具体情况
import (
"sync"
"sync/atomic"
"unsafe"
)
type ExtMutex struct {
m sync.Mutex
}
const (
mutexLocked = 1 << iota
mutexWoken
mutexStarving
mutexWaiterShift = iota
)
func (em *ExtMutex) TryLock() bool {
if atomic.CompareAndSwapInt32((*int32)(unsafe.Pointer(&em.m)), 0, mutexLocked) {
return true
}
old := atomic.LoadInt32((*int32)(unsafe.Pointer(&em.m)))
if old&(mutexLocked|mutexStarving|mutexWoken) != 0 {
return false
}
new := old | mutexLocked
return atomic.CompareAndSwapInt32((*int32)(unsafe.Pointer(&em.m)), old, new)
}
func (em *ExtMutex) Count() int32 {
v := atomic.LoadInt32((*int32)(unsafe.Pointer(&em.m)))
v = v >> mutexWaiterShift
v = v + (v & mutexLocked)
return v
}
func (em *ExtMutex) IsLocked() bool {
state := atomic.LoadInt32((*int32)(unsafe.Pointer(&em.m)))
return state&mutexLocked == mutexLocked
}
func (em *ExtMutex) IsWoken() bool {
state := atomic.LoadInt32((*int32)(unsafe.Pointer(&em.m)))
return state&mutexWoken == mutexWoken
}
func (em *ExtMutex) IsStarving() bool {
state := atomic.LoadInt32((*int32)(unsafe.Pointer(&em.m)))
return state&mutexStarving == mutexStarving
}