首页 > 其他分享 >go 锁

go 锁

时间:2023-01-17 21:13:52浏览次数:40  
标签:加锁 goroutine 模式 饥饿 Mutex go 唤醒

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

概念:

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

使用场景:

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

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

 

 

 

 

底层实现结构:

互斥锁对应的是底层结构是sync.Mutex结构体,,位于 src/sync/mutex.go

type Mutex struct {  
state int32  
sema  uint32
}

state表示锁的状态,有锁定、被唤醒、饥饿模式等,并且是用state的二进制位来标识的,不同模式下会有不同的处理方式:

 

 

 

 sema表示信号量,mutex阻塞队列的定位是通过这个变量来实现的,从而实现goroutine的阻塞和唤醒。

 

 

 

操作:

锁的实现一般会依赖于原子操作、信号量,通过atomic 包中的一些原子操作来实现锁的锁定,通过信号量来实现线程的阻塞与唤醒

加锁

通过原子操作cas加锁,如果加锁不成功,根据不同的场景选择自旋重试加锁或者阻塞等待被唤醒后加锁

 

 

 

func (m *Mutex) Lock() {
// Fast path: 幸运之路,一下就获取到了锁
if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {
return
}
// Slow path:缓慢之路,尝试自旋或阻塞获取锁
m.lockSlow()
}

解锁

通过原子操作add解锁,如果仍有goroutine在等待,唤醒等待的goroutine

 

 

 

func (m *Mutex) Unlock() {  
// Fast path: 幸运之路,解锁
new := atomic.AddInt32(&m.state, -mutexLocked)  
if new != 0 {  
// Slow path:如果有等待的goroutine,唤醒等待的goroutine
m.unlockSlow()
}  
}

注意点:

-  在 Lock() 之前使用 Unlock() 会导致 panic 异常
-  使用 Lock() 加锁后,再次 Lock() 会导致死锁(不支持重入),需Unlock()解锁后才能再加锁
-  锁定状态与 goroutine 没有关联,一个 goroutine 可以 Lock,另一个 goroutine 可以 Unlock

 

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

正常模式(非公平锁)

在刚开始的时候,是处于正常模式(Barging),也就是,当一个G1持有着一个锁的时候,G2会自旋的去尝试获取这个锁。当自旋超过4次还没有能获取到锁的时候,这个G2就会被加入到获取锁的等待队列里面,并阻塞等待唤醒。正常模式下,所有等待锁的goroutine按照FIFO(先进先出)的顺序等待,唤醒的goroutine不会直接拥有锁,而是会和新的请求锁的goroutine一起去竞争锁。新请求锁的goroutine具有优势,因为它正在CPU上执行,所以刚刚唤醒的goroutine有很大可能性在竞争中失败,就会切换到饥饿模式。

饥饿模式(公平锁)

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

starving = runtime_nanotime()-waitStartTime > 1e6

那么也不可能说永远的保持一个饥饿的状态,总归会有吃饱的时候,也就是总有那么一刻Mutex会回归到正常模式,那么回归正常模式必须具备的条件有以下几种:

1. G的执行时间小于1ms
2. 等待队列已经全部清空了

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

总结

对于两种模式,正常模式下的性能是最好的,goroutine 可以连续多次获取锁,饥饿模式解决了取锁公平的问题,但是性能会下降,其实是性能和公平的 一个平衡模式。

 

标签:加锁,goroutine,模式,饥饿,Mutex,go,唤醒
From: https://www.cnblogs.com/dadishi/p/17058690.html

相关文章

  • brew install mongodb
    admin@admindeMac-mini/%brewinstallmongodb-community@5.0fat......
  • hugo 安装使用
    安装go先在https://go.dev/doc/install下载压缩包,然后进行安装rm-rf/usr/local/go&&tar-C/usr/local-xzfgo1.19.5.linux-amd64.tar.gzexportPATH=$PATH:/us......
  • 我的Go+语言初体验——在Docker建立一个可以用Go+语言开发的容器环境(以Ubuntu容器为例
    前言​​“我的Go+语言初体验”|征文活动进行中......​​作为一名嵌入式软件工程师的我,在工作中使用高级语言开发的场景不多,但技术的迭代大部分偏向于应用层开发,身为程序......
  • go 使用redis
       import(redigo"github.com/gomodule/redigo/redis")  funcNewPool()*redigo.Pool{//return&redigo.Pool{//MaxIdle:3,......
  • golang无缓冲channel避坑点
    输出结果顺序:默认情况下,channel接收和发送数据都是阻塞的,除非另一端已经准备好。......
  • django4.1版本小demo
    1.安装anoconda,创建虚拟python3.9环境2.安装django,pipinstalldjango-i清华源安装较快3.新建一个django项目django-adminstartprojectdemo14.创建单个或者多......
  • golang: os.Write函数
    函数解析os.Write函数func(f*File)Write(b[]byte)(nint,errerror){//判断是否f为nil iferr:=f.checkValid("write");err!=nil{ return0,e......
  • GO语言基础语法
    切片与数组typePstruct{ namestring passwordint}funcmain(){ //p:=[3]P{ // {"wqdi",123}, // {"zhang",123123}, // {"qina",123123123}, //......
  • 基于Django的全栈个人博客
    基础但完整的三个DjangoFullstackBlog项目这三个用的都是基础的Django全栈,没有做前后端分离。前两个有完整教程,第三个教程无,但有redis缓存,后续可以三个对照着相互补充。......
  • Django Session 原理及配置和使用
    1、Django如何使用session会话1.1)session会话是通过中间件实现的,所以首先需要配置MIDDLEWAREMIDDLEWARE=[......'django.contrib.sessions.middleware.SessionM......