本文主要讲解“如何在Go Runtime中理解互斥体”,简单明了,易学易懂。请跟随边肖的思路,一起学习和学习“如何在Go Runtime中理解互斥体”。
同步。互斥是高级同步原语,是广大Go开发者开发应用的数据结构。现在它的内部实现逻辑很复杂,包括自旋和饥饿处理逻辑。它的底层在运行时使用一些低级的函数和一些原子方法。
运行时中的互斥体是在运行时中使用互斥体的同步原语。它提供了旋转和等待队列,但没有解决饥饿状态。此外,它的实现与sync不同。互斥体它不以方法的方式提供锁定/解锁,而是提供锁定/解锁功能,实现请求锁定和释放锁定。
今年年初,Dan Scales在运行时给锁增加了静态锁定等级的功能。他在运行时为独立于架构的锁定义了等级,并在运行时定义了锁的一些部分顺序(在这个锁之前允许持有哪些锁)。这是运行时锁的一个很大的变化,但遗憾的是没有设计文档来详细描述这个函数的设计。您可以通过提交注释(#0a820007)和代码中的注释来找出运行时内部锁的代码更改。
本质上,这个功能是用来检查锁定顺序是否按照文档设计顺序执行的。如果有任何违反设置顺序的情况,可能会发生死锁。因为缺少准确的文档,而且这个函数主要是用来检查运行时锁的执行顺序的,我在本文中就将这个逻辑抹掉。要在实际的Go运行时启动该检查,您需要设置变量GOEXPERIMENT=staticlockranking。
然后我们来看看互斥在运行时的数据结构的定义和锁/解锁的实现。
运行时mutex数据结构
运行时的互斥数据结构非常简单,如下所示,在runtime2.go中定义:
typemexcstruct { lockRankStruct//futex-basedimplitstatistsuite 32 key,//而sema-basedimpliasm * waim。//usettobaunion,butunisBreakRecisegc.keyuintpr }如果没有启用锁排序,lockrankstruct实际上是一个空结构:
TypelockRankStructstruct{}那么对于运行时的互斥来说,最重要的就是关键字段。这个字段对于不同的架构有不同的含义。
对于dragonfly、freebsd和linux架构,mutex将使用基于Futex的实现,key是uint32的值。Linux提供的Futex(快速用户空间互斥体)用于在用户空间建立锁和信号量。Go runtime封装了两种睡眠和唤醒当前线程的方法:
Futex sleep (addr uint32,valuint32,nsint64):原子操作` ifaddr==val {sleep} `。
Futex唤醒(addr * uint32,cntuint32):最多在addr唤醒线程cnt次。
对于其他架构,如aix、darwin、netbsd、openbsd、plan9、solaris和windows,mutex将使用基于sema的实现,关键是M * waitm。Go runtime封装了三种创建信号量和睡眠/唤醒的方法:
Func semacreate(mp *m):创建信号量。
Func semasleep(ns int64) int32:请求一个信号量,如果没有请求,它将休眠一段时间。
Funcsem唤醒(mp * m):唤醒mp。
基于这两种实现,分别有不同的lock和unl。
ock方法的实现,主要逻辑都是类似的,所以接下来我们只看基于Futex的lock/unlock。
请求锁lock
如果不使用lock ranking特性,lock的逻辑主要是由lock2实现的。
func lock(l *mutex) { lockWithRank(l, getLockRank(l)) } func lockWithRank(l *mutex, rank lockRank) { lock2(l) } func lock2(l *mutex) { // 得到g对象 gp := getg() // g绑定的m对象的lock计数加1 if gp.m.locks < 0 { throw("runtime·lock: lock count") } gp.m.locks++ // 如果有幸运光环,原来锁没有被持有,一把就获取到了锁,就快速返回了 v := atomic.Xchg(key32(&l.key), mutex_locked) if v == mutex_unlocked { return } // 否则原来的可能是MUTEX_LOCKED或者MUTEX_SLEEPING wait := v // 单核不进行spin,多核CPU情况下会尝试spin spin := 0 if ncpu > 1 { spin = active_spin } for { // 尝试spin,如果锁已经释放,尝试抢锁 for i := 0; i < spin; i++ { for l.key == mutex_unlocked { if atomic.Cas(key32(&l.key), mutex_unlocked, wait) { return } } // PAUSE procyield(active_spin_cnt) } // 再尝试抢锁, rescheduling. for i := 0; i < passive_spin; i++ { for l.key == mutex_unlocked { if atomic.Cas(key32(&l.key), mutex_unlocked, wait) { return } } osyield() } // 再尝试抢锁,并把key设置为mutex_sleeping,如果抢锁成功,返回 v = atomic.Xchg(key32(&l.key), mutex_sleeping) if v == mutex_unlocked { return } // 否则sleep等待 wait = mutex_sleeping futexsleep(key32(&l.key), mutex_sleeping, -1) } }
unlock
如果不使用lock ranking特性,unlock的逻辑主要是由unlock2实现的。
func unlock(l *mutex) { unlockWithRank(l) } func unlockWithRank(l *mutex) { unlock2(l) } func unlock2(l *mutex) { // 将key的值设置为mutex_unlocked v := atomic.Xchg(key32(&l.key), mutex_unlocked) if v == mutex_unlocked { throw("unlock of unlocked lock") } // 如果原来有线程在sleep,唤醒它 if v == mutex_sleeping { futexwakeup(key32(&l.key), 1) } //得到当前的goroutine以及和它关联的m,将锁的计数减1 gp := getg() gp.m.locks-- if gp.m.locks < 0 { throw("runtime·unlock: lock count") } if gp.m.locks == 0 && gp.preempt { // restore the preemption request in case we've cleared it in newstack gp.stackguard0 = stackPreempt } }
总体来说,运行时的mutex逻辑还不太复杂,主要是需要处理不同的架构的实现,它休眠唤醒的对象是m,而sync.Mutex休眠唤醒的对象是g。
感谢各位的阅读,以上就是“如何理解Go运行时中的Mutex”的内容了,经过本文的学习后,相信大家对如何理解Go运行时中的Mutex这一问题有了更深刻的体会,具体使用情况还需要大家实践验证。这里是,小编将为大家推送更多相关知识点的文章,欢迎关注!
内容来源网络,如有侵权,联系删除,本文地址:https://www.230890.com/zhan/42854.html