go笔记——锁

Go互斥锁的实现原理

Go sync包提供了两种锁类型:互斥锁sync.Mutex和读写互斥锁sync.RWMutex,都属于悲观锁。

概念:
Mutex是互斥锁,当一个goroutine获得了锁后,其他goroutine不能获取锁(只能存在一个写者或读者,不能同时读和写)

使用场景;
多个线程同时访问临界区,为保证数据的安全,锁住一些共享资源,以防止并发访问这些共享数据时可能导致的数据不一致问题。

获取锁的线程可以正常访问临界区,未获取到锁的线程等待锁释放后可以尝试获取锁

在这里插入图片描述

type Mutex struct {
state int32sema uint32
}

在这里插入图片描述

在这里插入图片描述

加锁过程

在这里插入图片描述

解锁过程

在这里插入图片描述

注意点:
1.在Lock()之前使用Unlock()会导致 panic 异常(直接解锁会painc)
2.使用Lock()加锁后,再次Lock()会导致死锁〈不支持重入),需Unlock()解锁后才能再加锁
3·锁定状态与goroutine没有关联,一个goroutine可以Lock,另一个goroutine 可以Unlock,意思就是groutine没有绑定锁


互斥锁正常模式和饥饿模式的区别

在Go一共可以分为两种抢锁的模式,一种是正常模式,另外一种是饥饿模式。

正常模式(非公平锁)

在刚开始的时候,是处于正常模式(Barging),也就是,当一个G1持有着一个锁的时候,G2会自旋的去尝试获取这个锁

当自旋超过4次还没有能获取到锁的时候,这个G2就会被加入到获取锁的等待队列里面,并阻塞等待唤醒

正常模式下,所有等待辍的goroutine 按照FIFO(先进先出)顺序等待。唤醒的goroutine不会直接拥有城,而是会和新请来顿的goroutine 竞争锁。新请求顿的 goroutine具有优势:
它正在CPU上执行,而且可能有好几个,所以刚刚唤醒的goroutine有很大可能在锁竞争中失败,长时间获取不到锁,就会切换到饥饿模式

饥饿模式(公平锁),为了解决公平问题

当一个goroutine等待锁时间超过1毫秒时,它可能会遇到饥饿问题。在版本1.9中,这种场景下Go Mutex切换到饥饿模式(handoff),解决饥饿问题。

饥饿模式下,直接把读交给等特队列中排在第一位的参与抢读也不会进入自旋状态,会直接进入等待队列的尾部,这样很好的解决了老的goroutine一直抢不到锁的场景。

那么也不可能说永远的保持一个饥饿的状态,总归会有吃饱的时候,也就是总有那么一刻Mutex会回归到正常模式,那么回归正常模式必须具备的条件有以下几种:
1.G的执行时间小于1ms
2.等特队列已经全部清空了

当满足上述两个条件的任意一个的时候,Mutex会切换回正常模式,而Go的抢锁的过程,就是在这个正常模式和饥饿模式中来回切换进行的。


自选的条件

线程没有获取到锁时常见有2种处理方式:

1.没有获取到锁的线程就一直循环等待判断该资源是否已经释放锁,这种锁也叫做自旋锁,它不用将线程阻塞起来,适用于并发低且程序执行时间短的场景,缺点是cpu占用较高

2.另外一种处理方式就是把自己阻塞起来,会释放CPU给其他线程,内核会将线程置为「睡眠」状态,等到锁被释放后,内核会在合适的时机唤醒该线程,适用于高并发场
景,缺点是有线程上下文切换的开销

Go语言中的Mutex实现了自旋与阻塞两种场景,当满足不了自旋条件时,就会进入阻塞

自旋的条件非常的苛刻

允许自旋的条件:
1.锁已被占用,并且锁不处于饥饿模式。
2.积累的自旋次数小于最大自旋次数(active_spin=4) .
3.cpu 核数大于1.
4.有空闲的P.
5.当前 goroutine所挂载的P下,本地待运行队列为空。


go读写锁的实现原理:

概念:

读写互斥锁RWMutex,是对Mutex的一个扩展,当一个goroutine获得了读锁后,其他gooutine可以获取读锁,但不能获取写锁;当一个goroutine获得了写锁后,其他goroutine既不能获取读锁也不能获取写锁(只能存在一个写者或多个读者,可以同时读)

底层数据结构

type Phiutex struct {
    w    Mutex              //复用互斥锁
    writerSem    uint32//信号量,用于写等待读
    readerSem   uint32//信号呈,用于读等特写
    readerCount int32 //当前执行读的goroutine数量
    readerwait int32 //被阻塞的准备读的goroutine的数量
    }

在这里插入图片描述

在这里插入图片描述

注意点:
1.读锁或写锁在Lock()之前使用Unlock()会导致 panic 异常
2.使用Lock()加锁后,再次Lock()会导致死锁(不支持重入),需Unlock()解锁后才能再加锁
3.锁定状态与goroutine没有关联,一个goroutine 可以RLock (Lock),另一个goroutine 可以RUnlock (Unlock)

互斥锁和读写锁的区别:
1.读写锁区分读者和写者,而互斥锁不区分
2.互斥锁同一时间只允许一个线程访问该对象,无论读写﹔读写锁同一时间内只允许一个写者,但是允许多个读者同时读对象。


可重入锁如何实现(递归锁)

概念:
可重入锁又称为递归锁,是指在同一个线程在外层方法获取锁的时候,在进入该线程的内层方法时会自动获取锁,不会因为之前已经获取过还没释放再次加锁导致死锁

为什么Go语言中没有可重入锁?
Mutex不是可重入的锁。Mutex的实现中没有记录哪个goroutine 拥有这把锁。理论上,任何goroutine都可以随意地Unock这把锁,所以没办法计算重入条件,并且Mtex重复Lock会导致死锁。

**如何实现可重入锁?**实现一个可重入锁需要这两点:
1.统计重入的次数
2.记住持有锁的线程


原子操作和锁的区别?

1.原子操作由底层硬件支持,而锁是基于原子操作+信号量完成的。若实现相同的功能,前者通常会更有效率

2.原子操作是单个指令的互斥操作﹔互斥锁/读写锁是一种数据结构,可以完成临界区(多个指令)的互斥操作,扩大原子操作的范围

3.原子操作是无锁操作,属于乐观锁;说起锁的时候,一般属于悲观锁

4.原子操作存在于各个指令/语言层级,比如*机器指令层级的原子操作",“汇编指令层级的原子操作”,“Go语言层级的原子操作"等。

5.锁也存在于各个指令/语言层级中,比如"机器指令层级的锁",“汇编指令层级的锁”,"Go语言层级的锁"等

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值