Golang Mutex
1、锁最本质的作用
保证原子性
2、mutex使用原则,注意事项
适用于并发编程,尽量减少加锁区域的逻辑
3、mutex的局限性
仅限于单个进程内操作
4、Mutex底层字段有哪些?sema是什么?
先来看看go语言的mutex的底层,我们可以在Go SDK的src/sync/mutex.go中找到
其中state是一个32位的值
第一位是加锁标记,第二位是唤醒标记,第三位是饥饿标记,之后的位数用来记录等待队列的数量。
mutexLocked = 1 << iota // mutex is locked mutexWoken mutexStarving mutexWaiterShift = iota
而sema(信号量,semaphore)是一种用于并发控制的机制
·资源计数:信号量维护一个资源计数,表示当前可用的资源数量
·获取操作(P操作):当一个协程(goroutine)想要获取资源时,会检查信号量的资源计数,如果·计数>0,说明有资源可用,可以获取资源,同时信号量计数减1;如果计数==0,表示没有可用资源,协程就会阻塞等待,直到有其他协程释放资源。
·释放操作(V操作):当一个协程使用完资源后,会释放资源。这会使得信号量计数+1,如果有协程正在等待这个资源,那么其中一个等待的协程会被唤醒并获取到资源。
跟本科学的操作系统中的知识一样,PV操作,同步机制信号量机制,貌似信号量机制还是Dijkstra提出的,这个荷兰人可以的
5、锁的快慢两种获取路径
先看源码
其中红色部分是快路径获取锁,0表示这个锁的初始状态
什么是快路径获取锁?
就是这个锁没有被锁定,目前并没有被获取,也没有什么队列在等待,像这种情况下就可以直接获得这个锁的占有权。快路径没有公平性可言。
否则绿色部分走的是慢路径获取锁
慢路径
只有慢路径,才有正常模式和饥饿模式。互斥锁基本就是这2种模式来回切换
正常模式:正在阻塞的等待者按照FIFO的顺序排队,但是被唤醒的等待者不一定拥有互斥锁,而是还要跟新到达的协程goroutine竞争互斥锁的所有权。
但是这就有个问题,在阻塞队列头被唤醒的那个等待者,它与新到来的goroutine竞争大概率会失败,因为新到来的goroutine他可能正在cpu上被执行,由它获取到锁肯定是最有效率的,因为不需要协程调度,开销小,性能也会更佳。这种情况下,被唤醒的等待者还是在队头,啥也没干,继续等,而这个等待时间有可能不可接受。所以引申出如果一个等待者超过1ms未能获取到它的互斥锁,他就会将互斥锁切换到饥饿模式。
饥饿模式:互斥锁的所有权直接从解锁的协程传递给队列前端的等待者。新到达的协程即使看到互斥锁似乎未被锁定也不会尝试获取,也不会尝试自旋(空转),相反,它们会将自己排到等待队列的末尾。
那如何从饥饿模式切换回正常模式呢:
1)被唤醒的等待者它是队列中最后一个等待者
2)它的等待时间少于1ms了,就认为可以切换为正常模式了
上文中提到了一嘴自旋,什么是自旋:
就是来到时发现锁资源已经被占用,那么就执行空逻辑去等待一小段时间,等待资源的释放并占用
形象来说就是假如有两个协程,其中一个已经占有锁了,新到达的那个发现你占有了,那我不一定去等待队列等待,我可能在这呆一会,你释放锁之后我紧接着就占有它。
慢路径获取锁,源码如下:
func (m *Mutex) lockSlow() { var waitStartTime int64 starving := false awoke := false iter := 0 old := m.state for { // Don't spin in starvation mode, ownership is handed off to waiters // so we won't be able to acquire the mutex anyway. if old&(mutexLocked|mutexStarving) == mutexLocked && runtime_canSpin(iter) { // Active spinning makes sense. // Try to set mutexWoken flag to inform Unlock // to not wake other blocked goroutines. if !awoke && old&mutexWoken == 0 && old>>mutexWaiterShift != 0 && atomic.CompareAndSwapInt32(&m.state, old, old|mutexWoken) { awoke = true } runtime_doSpin() iter++ old = m.state continue } new := old // Don't try to acquire starving mutex, new arriving goroutines must queue. if old&mutexStarving == 0 { new |= mutexLocked } if old&(mutexLocked|mutexStarving) != 0 { new += 1 << mutexWaiterShift } // The current goroutine switches mutex to starvation mode. // But if the mutex is currently unlocked, don't do the switch. // Unlock expects that starving mutex has waiters, which will not // be true in this case. if starving && old&mutexLocked != 0 { new |= mutexStarving } if awoke { // The goroutine has been woken from sleep, // so we need to reset the flag in either case. if new&mutexWoken == 0 { throw("sync: inconsistent mutex state") } new &^= mutexWoken } if atomic.CompareAndSwapInt32(&m.state, old, new) { if old&(mutexLocked|mutexStarving) == 0 { break // locked the mutex with CAS } // If we were already waiting before, queue at the front of the queue. queueLifo := waitStartTime != 0 if waitStartTime == 0 { waitStartTime = runtime_nanotime() } runtime_SemacquireMutex(&m.sema, queueLifo, 1) starving = starving || runtime_nanotime()-waitStartTime > starvationThresholdNs old = m.state if old&mutexStarving != 0 { // If this goroutine was woken and mutex is in starvation mode, // ownership was handed off to us but mutex is in somewhat // inconsistent state: mutexLocked is not set and we are still // accounted as waiter. Fix that. if old&(mutexLocked|mutexWoken) != 0 || old>>mutexWaiterShift == 0 { throw("sync: inconsistent mutex state") } delta := int32(mutexLocked - 1<<mutexWaiterShift) if !starving || old>>mutexWaiterShift == 1 { // Exit starvation mode. // Critical to do it here and consider wait time. // Starvation mode is so inefficient, that two goroutines // can go lock-step infinitely once they switch mutex // to starvation mode. delta -= mutexStarving } atomic.AddInt32(&m.state, delta) break } awoke = true iter = 0 } else { old = m.state } } if race.Enabled { race.Acquire(unsafe.Pointer(m)) } }
Golang RWMutex
1、读写锁与互斥锁的区别
互斥锁:同一时间只允许持锁的那一个协程访问被保护的资源,不论读写。
读写锁:在互斥锁的基础上进行了优化,将操作分为读操作和写操作,允许多个协程同时进行读操作,但写操作必须独占访问,从而提高读多写少场景的效率。
2、读写锁的应用场景
也就是读多写少的场景,比如:缓存、读取配置等。
3、读写锁的底层实现
我们可以在Go SDK的src/sync/rwmutex.go中找到
w
第1个字段,是一个互斥锁,只有写操作写锁的时候,才会去持有这个锁,才回去用到这个字段,而我们的读是不用这个字段的,写和写是通过这个字段去处理的,但是写和读不是通过这个字段去处理的。
writerSem
第2个字段,是一个信号量,告诉写操作什么时候可以写了,什么时候轮到我们的写锁去持有这一个锁。在多协程的情况下,之前可能有很多读操作,等之前的读操作都执行完之后,会向这个信号量发一个信号,然后写锁持有锁,那么我们的写操作可以正式的进行操作了。
readerSem
第3个字段,也一个信号量,告诉读操作什么时候可以正式读了,什么时候轮到我们的读锁去持有这一个读。在多协程的情况下,之前可能有很多写操作,等之前的写操作都执行完之后,会向这个信号量发一个信号,然后读锁持有锁,那么我们的读操作可以正式的进行操作了。
也就是写操作等待之前的读操作完成,读操作等待之前的写操作完成。
readerCount
第4个字段,表示目前有多少个读协程,包括正在等待的和已经获取到锁且没有释放的,是一个总量。
atomic.Int32类型,
readerwait
第5个字段,记录正在离开的读操作的数量
写锁进来之前的读锁的数量,会转移到readerwait,当readerwait清空之后,会向writerSem写一个信号量。
若只有读和读,那么w字段用不上。
readercount,来一个读协程我就+1,走一个读协程我就-1。
提到的atomic.Int32
是Go语言标准库中的一个类型,专门用于在并发环境中进行原子操作,确保多线程环境下数据的一致性和安全性,有若干个方法:
