Golang 锁机制
树獭叔叔 人气:01. sync.Mutex详解
sync.Mutex
是Go中的互斥锁,通过.lock()
方法上锁,.unlock()
方法解锁。需要注意的是,因为Go函数值传递的特点,sync.Mutex
通过函数传递时,会进行一次拷贝,所以传递过去的锁是一把全新的锁,大家在使用时要注意这一点,另外sync.Mutex
是非重入锁,这一点要与Java中的锁区分。
type Mutex { state int32 sema uint32 }
上面数据结构中的state
最低三位分别表示 mutexLocked、mutexWoken 和 mutexStarving,剩下的位置用来表示当前有多少个 Goroutine 等待互斥锁的释放:
32 3 2 1 0 | | | | | | | | | | v-----------------------------------------------v-------------v-------------v-------------+ | | | | v | waitersCount |mutexStarving| mutexWoken | mutexLocked | | | | | | +-----------------------------------------------+-------------+-------------+-------------+
- mutexLocked — 表示互斥锁的锁定状态;
- mutexWoken — 表示从正常模式被从唤醒;
- mutexStarving — 当前的互斥锁进入饥饿状态;
- waitersCount — 当前互斥锁上等待的 goroutine 个数;
2. RWMutex详解
type RWMutex struct { w Mutex // 复用互斥锁 writerSem uint32 // 写锁监听读锁释放的信号量 readerSem uint32 // 读锁监听写锁释放的信号量 readerCount int32 // 当前正在执行读操作的数量 readerWait int32 // 当写操作被阻塞时,需要等待读操作完成的个数 }
- 读操作如何防止并发读写问题的?
RLock(): 申请读锁,每次执行此函数后,会对readerCount++,此时当有写操作执行Lock()时会判断readerCount>0,就会阻塞。
RUnLock(): 解除读锁,执行readerCount–,释放信号量唤醒等待写操作的goroutine。
- 写操作如何防止并发读写、并发写写问题?
Lock(): 申请写锁,获取互斥锁,此时会阻塞其他的写操作。并将readerCount 置为 -1,当有读操作进来,发现readerCount = -1, 即知道有写操作在进行,阻塞。
Unlock(): 解除写锁,会先通知所有阻塞的读操作goroutine,然后才会释放持有的互斥锁。
- 写操作的饥饿问题?
这是由于写操作要等待读操作结束后才可以获得锁,而写操作在等待期间可能还有新的读操作持续到来,如果写操作等待所有读操作结束,很可能会一直阻塞,这种现象称之为写操作被饿死。
通过RWMutex结构体中的readerWait属性可完美解决这个问题。
当写操作到来时,会把RWMutex.readerCount值拷贝到RWMutex.readerWait中,用于标记排在写操作前面的读者个数。
前面的读操作结束后,除了会递减RWMutex.readerCount,还会递减RWMutex.readerWait值,当RWMutex.readerWait值变为0时唤醒写操作。
3. sync.Map详解
一般情况下解决并发读写 map 的思路是加一把大锁,或者把一个 map 分成若干个小 map,对 key 进行哈希,只操作相应的小 map。前者锁的粒度比较大,影响效率;后者实现起来比较复杂,容易出错。
而使用 sync.map
之后,对 map 的读写,不需要加锁。并且它通过空间换时间的方式,使用 read 和 dirty 两个 map 来进行读写分离,降低锁时间来提高效率。
type Map struct { mu Mutex read atomic.Value // readOnly dirty map[interface{}]*entry misses int } // readOnly is an immutable struct stored atomically in the Map.read field. type readOnly struct { m map[interface{}]*entry amended bool // true if the dirty map contains some key not in m. } type entry struct { p unsafe.Pointer // *interface{} }
在进行读操作的时候,会先在read中找,没有命中的话会锁住dirty并且寻找,如果找到了miss计数+1,超过阈值时将dirty赋值给read;
在进行添加操作时,直接在dirty中添加;
在进行修改操作时,先改read,再改dirty;
在进行删除操作时,将read中加上amended标记,dirty中直接删除。
4. 原子操作 atomic.Value
愿此操作的底层是靠 MESI 缓存一致性协议来维持的。
Go的 atomic.Value 需要注意应该放入只读对象。
//atomic.Value源码 type Value struct { v interface{} // 所以可以存储任何类型的数据 } // 空 interface{} 的内部表示格式,作用是将interface{}类型分解,得到其中两个字段 type ifaceWords struct { typ unsafe.Pointer data unsafe.Pointer } // 取数据就是正常走流程 func (v *Value) Load() (x interface{}) { vp := (*ifaceWords)(unsafe.Pointer(v)) typ := LoadPointer(&vp.typ) if typ == nil || uintptr(typ) == ^uintptr(0) { // 第一次还没写入 return nil } // 构造新的interface{}返回出去 data := LoadPointer(&vp.data) xp := (*ifaceWords)(unsafe.Pointer(&x)) xp.typ = typ xp.data = data return } // 写数据(如何保证数据完整性) func (v *Value) Store(x interface{}) { if x == nil { panic("sync/atomic: store of nil value into Value") } // 绕过 Go 语言类型系统的检查,与任意的指针类型互相转换 vp := (*ifaceWords)(unsafe.Pointer(v)) // 旧值 xp := (*ifaceWords)(unsafe.Pointer(&x)) // 新值 for { // 配合CompareAndSwap达到乐观锁的功效 typ := LoadPointer(&vp.typ) if typ == nil { // 第一次写入 runtime_procPin() // 禁止抢占 if !CompareAndSwapPointer(&vp.typ, nil, unsafe.Pointer(^uintptr(0))) { runtime_procUnpin() // 没有抢到锁,说明已经有别的线程抢先完成赋值,重新进入循环 continue } // 首次赋值 StorePointer(&vp.data, xp.data) StorePointer(&vp.typ, xp.typ) runtime_procUnpin() // 写入成功,解除占用状态 return } if uintptr(typ) == ^uintptr(0) { // 第一次写入还未完成,继续等待 continue } // 两次需要写入相同类型 if typ != xp.typ { panic("sync/atomic: store of inconsistently typed value into Value") } StorePointer(&vp.data, xp.data) return } } // 禁止抢占,标记当前G在M上不会被抢占,并返回当前所在P的ID。 func runtime_procPin() // 解除G的禁止抢占状态,之后G可被抢占。 func runtime_procUnpin()
5. 使用小技巧
- 减小临界区域(减少锁的持有时间)
var m sync.Mutex func DoSth() { // do sth1 func() { u.lock() defer m.unlock() // do sth2 }() // do sth3 }
如上所示,如果do sth3中是很费时的io操作,使用这个技巧可以将临界区减小,提高性能,不过,如果本身临界区就不大,锁操作后续没有什么费时操作,那么也就没有必要这样操作了。
- 减小锁的粒度
在高并发场景下,用锁的数量来换取并发效率,类似于java中ConcurrentHashmap的分段锁思想,增加锁的数量,减少一把锁控制的数据量。
- 读写分离(读写锁): RWMutex,sync.Map
在读多写少的情景下,可以使用读写锁,提高读操作的并发性能。
- 使用原子操作
原子操作是CPU指令级的操作,不会触发g调度机制。,不阻塞执行流
加载全部内容