Go语言底层原理互斥锁的实现原理
来源:脚本之家
时间:2022-12-28 16:26:08 446浏览 收藏
怎么入门Golang编程?需要学习哪些知识点?这是新手们刚接触编程时常见的问题;下面golang学习网就来给大家整理分享一些知识点,希望能够给初学者一些帮助。本篇文章就来介绍《Go语言底层原理互斥锁的实现原理》,涉及到互斥锁,有需要的可以收藏一下
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的阻塞和唤醒
addr = &sema func semroot(addr *uint32) *semaRoot { return &semtable[(uintptr(unsafe.Pointer(addr))>>3)%semTabSize].root } root := semroot(addr) root.queue(addr, s, lifo) root.dequeue(addr) var semtable [251]struct { root semaRoot ... } type semaRoot struct { lock mutex treap *sudog // root of balanced tree of unique waiters. nwait uint32 // Number of waiters. Read w/o the lock. } type sudog struct { g *g next *sudog prev *sudog elem unsafe.Pointer // 指向sema变量 waitlink *sudog // g.waiting list or semaRoot waittail *sudog // semaRoot ... }
操作
锁的实现一般会依赖于原子操作、信号量,通过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 互斥锁正常模式和饥饿模式的区别?
在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
正常模式下,所有等待锁的 goroutine 按照 FIFO(先进先出)顺序等待。唤醒的goroutine 不会直接拥有锁,而是会和新请求锁的 goroutine 竞争锁。新请求锁的 goroutine 具有优势:它正在 CPU 上执行,而且可能有好几个,所以刚刚唤醒的 goroutine 有很大可能在锁竞争中失败,长时间获取不到锁,就会切换到饥饿模式
那么也不可能说永远的保持一个饥饿的状态,总归会有吃饱的时候,也就是总有那么一刻Mutex会回归到正常模式,那么回归正常模式必须具备的条件有以下几种:
- G的执行时间小于1ms
- 等待队列已经全部清空了
当满足上述两个条件的任意一个的时候,Mutex会切换回正常模式,而Go的抢锁的过程,就是在这个正常模式和饥饿模式中来回切换进行的。
delta := int32(mutexLocked - 1>mutexWaiterShift == 1 { delta -= mutexStarving } atomic.AddInt32(&m.state, delta)
小结:
对于两种模式,正常模式下的性能是最好的,goroutine 可以连续多次获取锁,饥饿模式解决了取锁公平的问题,但是性能会下降,其实是性能和公平的 一个平衡模式。
Go 互斥锁允许自旋的条件?
线程没有获取到锁时常见有2种处理方式:
- 一种是没有获取到锁的线程就一直循环等待判断该资源是否已经释放锁,这种锁也叫做自旋锁,它不用将线程阻塞起来, 适用于并发低且程序执行时间短的场景,缺点是cpu占用较高
- 另外一种处理方式就是把自己阻塞起来,会释放CPU给其他线程,内核会将线程置为「睡眠」状态,等到锁被释放后,内核会在合适的时机唤醒该线程,适用于高并发场景,缺点是有线程上下文切换的开销
Go语言中的Mutex实现了自旋与阻塞两种场景,当满足不了自旋条件时,就会进入阻塞
允许自旋的条件:
- 锁已被占用,并且锁不处于饥饿模式。
- 积累的自旋次数小于最大自旋次数(active_spin=4)。
- cpu 核数大于 1。
- 有空闲的 P。
- 当前 goroutine 所挂载的 P 下,本地待运行队列为空。
if old&(mutexLocked|mutexStarving) == mutexLocked && runtime_canSpin(iter) { ... runtime_doSpin() continue } func sync_runtime_canSpin(i int) bool { if i >= active_spin || ncpu自旋:
func sync_runtime_doSpin() { procyield(active_spin_cnt) }如果可以进入自旋状态之后就会调用
runtime_doSpin
方法进入自旋,doSpin
方法会调用procyield(30)
执行30次PAUSE
指令,什么都不做,但是会消耗CPU时间本篇关于《Go语言底层原理互斥锁的实现原理》的介绍就到此结束啦,但是学无止境,想要了解学习更多关于Golang的相关知识,请关注golang学习网公众号!
-
200 收藏
-
312 收藏
-
117 收藏
-
446 收藏
-
426 收藏
-
233 收藏
-
322 收藏
-
181 收藏
-
316 收藏
-
244 收藏
-
300 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 542次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 507次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 497次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 484次学习
-
- 俭朴的心情
- 这篇文章内容真及时,太细致了,真优秀,码起来,关注楼主了!希望楼主能多写Golang相关的文章。
- 2023-03-25 08:28:45
-
- 优美的猎豹
- 写的不错,一直没懂这个问题,但其实工作中常常有遇到...不过今天到这,帮助很大,总算是懂了,感谢师傅分享文章!
- 2023-01-13 17:53:44
-
- 傲娇的水壶
- 这篇文章内容太及时了,很详细,很棒,mark,关注大佬了!希望大佬能多写Golang相关的文章。
- 2023-01-11 08:23:28
-
- 激昂的小虾米
- 很棒,一直没懂这个问题,但其实工作中常常有遇到...不过今天到这,看完之后很有帮助,总算是懂了,感谢作者分享博文!
- 2023-01-10 06:48:26
-
- 执着的雪碧
- 好细啊,收藏了,感谢老哥的这篇技术贴,我会继续支持!
- 2023-01-01 02:56:23