golang的并发控制技术使用及原理(二:Mutex,RWMutex,Once)

go的并发控制手段有
channel,waitgroup,context,
sync包中的rwlock,lock,pool,Once,cond,map等。

channel,waitgroup,context,在另一篇文章中已有

这里主要是Mutex,RWMutex,Once的实现原理

目录

一:Mutex

底层结构

状态解释

二:RWMutex

底层结构

方法实现逻辑

阻塞分析

三:Once

底层结构

实现原理


一:Mutex

底层结构

type Mutex struct {
    state int32
    sema uint32
}

一个信号量用来挂起和唤醒当前协程,一个32位的状态用来标识互斥锁的状态。
Locked: 表示该Mutex是否已被锁定, 0: 没有锁定 1: 已被锁定。
Woken: 表示是否有协程已被唤醒, 0: 没有协程唤醒 1: 已有协程唤醒, 正在加锁过程中。
Starving: 表示该Mutex是否处理饥饿状态, 0: 没有饥饿 1: 饥饿状态, 说明有协程阻塞了超过1ms。
Waiter: 表示阻塞等待锁的协程个数, 协程解锁时根据此值来判断是否需要释放信号量。

状态解释

正常简单的加锁一般只用到locked和waiter两个状态。
woken状态位是为了提高性能使用的状态位。
加锁时, 如果当前Locked位为1, 说明该锁当前由其他协程持有, 尝试加锁的协程并不是马上转入阻塞, 而是会持续的探测Locked位是否变为0, 这个过程即为自旋过程。
自旋时间很短, 但如果在自旋过程中发现锁已被释放,那么就检查该状态位,如果有自旋的,那么就不释放信号量唤醒新的协程。那么自旋的协程可以立即获取锁。 此时即便有协程被唤醒也无法获取锁, 只能再次阻塞。
自旋的好处是, 当加锁失败时不必立即转入阻塞, 有一定机会获取到锁, 这样可以避免协程的切换。
自旋的条件
1.自旋次数要足够小, 通常为4, 即自旋最多4次
2.CPU核数要大于1, 否则自旋没有意义, 因为此时不可能有其他协程释放锁
3.协程调度机制中的Process数量要大于1, 比如使用GOMAXPROCS()将处理器设置为1就不能启用自旋
4.协程调度机制中的可运行队列必须为空, 否则会延迟协程调度
自旋的好处
自旋的优势是更充分的利用CPU, 尽量避免协程切换。 因为当前申请加锁的协程拥有CPU, 如果经过短时间的自旋可以
获得锁, 当前协程可以继续运行, 不必进入阻塞状态。
自旋的问题:
如果自旋过程中获得锁, 那么之前被阻塞的协程将无法获得锁, 如果加锁的协程特别多, 每次都通过自旋获得锁, 那
么之前被阻塞的进程将很难获得锁, 从而进入饥饿状态。
为了避免协程长时间无法获取锁, 自1.8版本以来增加了一个状态, 即Mutex的Starving状态。 

Starving:默认情况下, Mutex的模式为normal。
该模式下, 协程如果加锁不成功不会立即转入阻塞排队, 而是判断是否满足自旋的条件, 如果满足则会启动自旋过程,尝试抢锁。
Starving模式:
自旋过程中能抢到锁, 一定意味着同一时刻有协程释放了锁, 我们知道释放锁时如果发现有阻塞等待的协程, 还会释
放一个信号量来唤醒一个等待协程, 被唤醒的协程得到CPU后开始运行, 此时发现锁已被抢占了, 自己只好再次阻塞,
不过阻塞前会判断自上次阻塞到本次阻塞经过了多长时间, 如果超过1ms的话, 会将Mutex标记为”饥饿”模式, 然后
再阻塞。
处于饥饿模式下, 不会启动自旋过程, 也即一旦有协程释放了锁, 那么一定会唤醒协程, 被唤醒的协程将会成功获取
锁, 同时也会把等待计数减1。

注意点:
1.加锁忘记解锁会导致死锁,
2.重复解锁会Panic
所以编程注意点:
1.加锁后立即使用defer对其解锁
2.加锁和解锁最好出现在同一个层次的代码块中, 比如同一个函数。最好不要将锁传递出去解锁。

 

 

 

二:RWMutex

读写锁用于读频率大于写频率的并发访问资源的场景.
使用读写锁, 多个读操作可以同时持有锁, 并发能力将大大提升。
读写锁能够解决的问题:
写锁需要阻塞写锁: 一个协程拥有写锁时, 其他协程写锁定需要阻塞
写锁需要阻塞读锁: 一个协程拥有写锁时, 其他协程读锁定需要阻塞
读锁需要阻塞写锁: 一个协程拥有读锁时, 其他协程写锁定需要阻塞
读锁不能阻塞读锁: 一个协程拥有读锁时, 其他协程也可以拥有读锁

底层结构

type RWMutex struct {
    w Mutex //写阻塞阻塞写阻塞
    writerSem uint32 //写操作被读者阻塞的信号量
    readerSem uint32 //读操作被写者阻塞的信号量
    readerCount int32 //记录读者个数
    readerWait int32 //记录写阻塞时读者个数
}

底层结构有一个互斥锁,用于写锁阻塞写锁。
读阻塞,写阻塞信号量,用于挂起和唤醒协程。
以及读者个数和写阻塞时读者个数的两个计数器。用于读写锁的互相阻塞。

方法实现逻辑

lock:lock时先获取互斥锁,然后readercount自减一个最大读者数量变为负值,再加回去得到最开始的readercount,判断大于0则挂起当前goroutinue,并且将最原本的readercount保存到readerwait中。(自减变为负值时为了阻塞后面的读锁,再加回去得到原来的值判断大于0则阻塞时为了被读操作阻塞自己的写操作,保存到readerwait中是为了读释放锁时释放因读阻塞的写阻塞。)

unlock时先自增readercount变为正值,再循环释放readercount个信号量。最后释放互斥锁。(自增readercount是为了不阻塞后面的读操作。释放readercount是为了唤醒之前挂起的读阻塞,最后释放互斥锁是为了释放写阻塞。)

rlock:累加readercount自增1,判断readercount<0则挂起当前协程(只被lock阻塞,lock时将该值减为负)

runlock:先readercount自减1,如果小于0再readerwait自减1,如果readerwait==0则释放一个写阻塞信号量。(因为不会有多个写操作因为读而阻塞,因为多个写由互斥锁阻塞。)

阻塞分析


写操作阻塞写操作:使用互斥锁互相阻塞
写操作阻塞读操作:利用readercount,写操作时将其减去rwmutexMaxReaders(1<<30位,即1的30次方),读操作时判断如果readercount<0则阻塞。
读操作阻塞写操作。写操作时获取互斥锁成功后,说明没有写操作互相阻塞,然后获取readercount的大小,如果为0则阻塞自己。
读操作不阻塞读操作,rlock时只对readercount做判断,而readercount只会被写操作减为负值。

 

 

三:Once

once用于限制只执行一次的代码,在并发过程中,多次调用只会执行第一次。后面的调用不会执行。

底层结构

type Once struct {
    m    Mutex
    done uint32
}

实现原理

超级简单,利用原子操作来判断,如果已经是1了,那么直接返回,否则将值改为1,然后执行一次传入的函数。

func (o *Once) Do(f func()) {
	if atomic.LoadUint32(&o.done) == 1 {
		return
	}
	// Slow-path.
	o.m.Lock()
	defer o.m.Unlock()
	if o.done == 0 {
		defer atomic.StoreUint32(&o.done, 1)
		f()
	}
}

 

 

 

 

 

 

 

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值