首页 > 其他分享 >Go 语言中 sync.Mutex 的实现

Go 语言中 sync.Mutex 的实现

时间:2024-04-15 16:14:27浏览次数:20  
标签:唤醒 goroutine sync Mutex 自旋 Go 等待

锁的获取和释放模式

先理解两种不同的锁的获取和释放模式"Barging" 和 "Handoff",它们影响着等待锁的 goroutines 的行为。

Barging(插队)

在 Barging 模式下,当一个锁被释放时,任何尝试获取该锁的 goroutine 都有机会立即抢占("插队")并尝试获取锁,而不管是否有其他 goroutines 正在等待。这意味着新到达的 goroutine 可能会在等待的 goroutine 之前获取锁,即使那些 goroutine 已经在等待队列中等待了一段时间。

优点:是它可以减少唤醒等待线程的延迟,因为不需要显式地从等待队列中选择一个线程并唤醒它。这可以提高性能,特别是在锁的竞争不是很激烈的情况下。

缺点:是可能导致 "饥饿",即某些 goroutine 可能会被不断地插队,从而无法及时获取锁。

如上图,Barging 模式既会唤醒等待者 G2,G3 作为新到达的 gorouting 也有机会直接获得锁

 

Handoff(交接)

在 Handoff 模式下,当一个锁被释放时,持有锁的 goroutine 显式地将锁交给等待队列中的下一个 goroutine。这意味着锁的所有权从释放锁的 goroutine 直接传递给等待队列中的一个特定 goroutine,而不允许其他 goroutine 插队。

优点:是它可以防止饥饿,因为等待时间最长的 goroutine 将被优先考虑。这种模式确保了公平性,因为每个 goroutine 都会按照它们到达等待队列的顺序获得服务。

缺点:可能会引入额外的延迟,因为需要显式地管理等待队列,并在每次释放锁时唤醒特定的 goroutine。

如上图,Handoff 模式唤醒等待队列中的 G2,之后直接把锁交接给 G2,即使唤醒过程中 G3、G4 正在请求锁

 

Go 语言中的实现

在 Go 语言的 sync.Mutex 实现中,为了平衡性能和公平性,采用了一种混合的策略。在某些情况下,它允许 Barging,以减少唤醒等待 goroutine 的开销;在其他情况下,它使用 Handoff,以确保长时间等待的 goroutine 最终能够获取锁,从而防止饥饿。

Go 1.9 引入了一些改进,使得 sync.Mutex 更加倾向于公平性,通过引入一个饥饿状态,当一个 goroutine 等待超过一定的时间阈值(1ms)时,它会进入饥饿状态。在饥饿状态下,锁的所有权会直接从解锁的 goroutine 交给等待队列中的下一个 goroutine,这类似于 Handoff 模式。当没有 goroutine 处于饥饿状态时,锁的获取更加自由,可能会出现 Barging。

这种混合策略旨在在高并发场景下提供良好的性能,同时在锁竞争激烈时保持足够的公平性,以避免饥饿问题。

 

自旋 

自旋(spinning)是一种优化技术,用于在某些情况下避免 goroutine 在等待锁时立即进入休眠状态。自旋可以让 goroutine 在短时间内忙等(busy-wait),以期在这段时间内锁被释放,从而避免了系统调用的开销和上下文切换的成本。

自旋在 Go 的 sync.Mutex 中是这样使用的:

自旋尝试:当一个 goroutine 尝试获取一个已经被其他 goroutine 持有的锁时,它会执行一个有限次数的自旋尝试。在这个过程中,goroutine 会在用户空间中忙等,检查锁是否已经被释放。

退让:如果在自旋尝试期间锁没有被释放,goroutine 可能会调用 runtime.Gosched() 或类似的函数来让出 CPU 时间片,给其他 goroutine 执行的机会。这样做可以减少 CPU 的无效消耗,尤其是在单核心或者核心数较少的情况下。

阻塞:如果自旋后锁仍然不可用,goroutine 最终会停止自旋,并通过更重的同步机制(如 futex 或其他内核同步原语)进入休眠状态,等待被唤醒。

 

自旋与 Barging 和 Handoff 模式的关系

与 Barging 的关系:自旋与 Barging 模式结合得很好,因为在 Barging 模式下,锁一旦被释放,任何 goroutine 都有机会获取它。因此,自旋的 goroutine 可能会在锁释放时立即获取到锁,而不需要被唤醒。

与 Handoff 的关系:在 Handoff 模式下,锁的所有权直接从释放锁的 goroutine 传递给等待队列中的下一个 goroutine。在这种情况下,自旋可能不那么有效,因为锁的获取是预定的,不会立即发生。但是,如果等待队列中没有饥饿的 goroutine,那么自旋的 goroutine 仍然有机会在锁释放时立即获取它。

Go 语言的运行时会根据当前的情况(如锁的竞争程度和处理器的数量)来决定是否使用自旋,以及自旋的次数。这是一个经过调优的决策,目的是在减少延迟和避免浪费 CPU 资源之间找到平衡点。

sync_runtime_canSpin 判断是否可以进入自旋

// Active spinning for sync.Mutex.
//go:linkname sync_runtime_canSpin sync.runtime_canSpin
//go:nosplit
func sync_runtime_canSpin(i int) bool {
    // sync.Mutex is cooperative, so we are conservative with spinning.
    // Spin only few times and only if running on a multicore machine and
    // GOMAXPROCS>1 and there is at least one other running P and local runq is empty.
    // As opposed to runtime mutex we don't do passive spinning here,
    // because there can be work on global runq or on other Ps.
    if i >= active_spin || ncpu <= 1 || gomaxprocs <= int32(sched.npidle+sched.nmspinning)+1 {
        return false
    }
    if p := getg().m.p.ptr(); !runqempty(p) {
        return false
    }
    return true
}

 

 

如果可以自选,就通过 sync_runtime_doSpin 进入自旋,对应执行 30 次 PAUSE 指令

PAUSE 指令会告诉 CPU 我当前处于处于自旋状态,这时候 CPU 会针对性的做一些优化,并且在执行这个指令的时候 CPU 会降低自己的功耗,减少能源消耗

//go:linkname sync_runtime_doSpin sync.runtime_doSpin
//go:nosplit
func sync_runtime_doSpin() {
    procyield(active_spin_cnt)
}

TEXT runtime·procyield(SB),NOSPLIT,$0-0
    MOVL    cycles+0(FP), AX
again:
    PAUSE
    SUBL    $1, AX
    JNZ    again
    RET

 

Mutex 源码结构

// A Mutex is a mutual exclusion lock.
// The zero value for a Mutex is an unlocked mutex.
//
// A Mutex must not be copied after first use.
type Mutex struct {
    state int32
    sema  uint32
}

mutexLocked = 1 << iota // mutex is locked
mutexWoken
mutexStarving
mutexWaiterShift = iota

  

state:4 字节的 Int32 类型,最低的 3 个 bit 表示锁的状态,分别是 mutexLocked  mutexWoken mutexStarving ,剩下的 bit 用于统计当前在等待锁的 goroutine 数量

sema:控制 gorouting 在获取锁过程当中的休眠和唤醒,在 Linux 系统中,信号量可以通过多种方式实现,其中之一是使用 "futex"(快速用户空间互斥锁)。futex 是一种系统调用,它提供了一种在用户空间线程之间进行高效等待和唤醒的机制

 

Lock 方法

Fast Path:首先尝试一个快速路径,如果锁是空闲的(state 为 0),就通过原子操作 CompareAndSwapInt32 尝试将其设置为 mutexLocked 状态。如果成功,就直接返回,表示获取锁成功。

Slow Path:如果快速路径失败,表示锁已经被其他 goroutine 持有,那么就进入慢路径。在慢路径中,会有一个循环,goroutine 会在这里等待,直到它能够获取锁。

Starvation Detection: 代码中有对饥饿状态的检测,如果一个 goroutine 长时间等待锁,它可能会进入饥饿模式,这时它会被优先唤醒。

Waiters Counting: 如果锁已经被持有,goroutine 会增加等待者的计数,并可能进入休眠状态,等待锁被释放。

Waking Up: 当锁被释放时,等待的 goroutine 会被唤醒。如果有多个等待者,唤醒操作会尝试保持公平性,避免某些 goroutine 饥饿。

 

Unlock 方法

Fast Path: 通过原子操作减少 state 的值来释放锁。如果减少后的值表明锁已经是未锁定状态,那么会抛出 panic,因为这意味着试图释放一个未锁定的互斥锁。

No Waiters: 如果没有等待者,或者已经有一个 goroutine 被唤醒或处于饥饿状态,那么就没有必要进行唤醒操作。

Wake Someone Up: 如果有等待者,那么会释放一个信号量 sema 来唤醒一个等待的 goroutine。

 

参考:

1. https://lailin.xyz/post/go-training-week3-sync.html#RWMutex

2. https://pkg.go.dev/sync 

标签:唤醒,goroutine,sync,Mutex,自旋,Go,等待
From: https://www.cnblogs.com/orchidzjl/p/18136167

相关文章

  • BinGoo系列之Socket组件《三、客户端+服务端组件的封装》 控件版(转)
    简介:继【C#原生Socket网络通讯】BinGoo系列之Socket服务端+客户端 之后,进一步封装的原生socket网络通讯组件。在此之前的版本还是要写一部分绑定委托事件代码,新版通讯类库将所有的消息机制全部封装成事件。只需拖动组件至窗体,直接双击组件注册事件,无需再写有关socket的代码,......
  • centos7安装golang最新版1.21.1
    #先卸载旧的golangyumremovegolang#然后找到最新版本https://golang.google.cn/dl/#下载安装cd/usr/local/src wgethttps://golang.google.cn/dl/go1.21.1.linux-amd64.tar.gztar-zxvfgo1.21.1.linux-amd64.tar.gz-C/usr/local/#增加配置文件vim/etc/profi......
  • Golang交替打印奇偶数
    packagemainimport( "fmt" "sync")varwgsync.WaitGroupfuncmain(){ evenCh,oddCh:=make(chanbool,1),make(chanbool,1) deferclose(evenCh) deferclose(oddCh) wg=sync.WaitGroup{} wg.Add(1) goprintNumbersSequent......
  • go语言连接Mogdb
    go语言连接Mogdb本文出处:https://www.modb.pro/db/3880921.环境介绍[root@mogdb-kernel-0004src]#goversion //yum安装的gogoversiongo1.16.13linux/amd64[root@mogdb-kernel-0004src]#cat/etc/redhat-releaseCentOSLinuxrelease7.6.1810(Core)2.获取p......
  • mongo内存管理之cache占用过高,影响正常业务
    云数据库DDS实例shard连接数异常激增影响业务的事件,问题分析如下保障现象:在当天22日21:50-22:21时间段内,连接数异常激增,同时伴随有大量慢日志,实例整体响应速度下降。3月23日19:35贵司报障该实例再次出现shard连接数激增的现象。排查处理:经过排查发现3月22日21:50-......
  • async与await暂停作用
    1.asyncfunctionRequest(){awaitnewPromise(res=>{setTimeout(()=>{console.log(1)res()},1000)})console.log(4);newPromise(res=>{setTimeout(()=>{......
  • jangow01
    虚拟机配置将攻击机和靶机放在同一网段下即可web渗透IP扫描nmap-sP192.168.11.0/24详细扫描nmap-A192.168.11.42探测web网站发现一可执行命令的地方,写入一句话木马,用蚁剑链接链接成功用蚁剑写文件,反弹链接,注意只有443端口出网web网站访问该文件攻击机监听......
  • Godot.NET C#IOC重构(3):视差景深背景
    目录相关链接前言目标效果景深效果实现无限长背景重复景深相关链接十分钟制作横版动作游戏|Godot4教程《勇者传说》#0前言这次来学习如何设置景深目标效果景深效果实现无限长背景只要开起了Mirror,Godot就会进行无限自我复制重复景深......
  • 利用Sqlmap API接口联动Google Hacking批量SQL注入检测
    目录前言slghack自动化搜集URLSqlmapAPI脚本slghack_sqli前言挖掘SQL注入漏洞的一种方式就是通过GoogleHacking搜索那些可能存在SQL的URL,然后手工的探测注入点。但是现在的SQL注入漏洞的网站是比较少的了,所以这样一个一个手工测效率有一点低。sqlmap比较好的一点是可批量扫描......
  • Go实践:用Sync.Map实现简易内存缓存系统
    介绍定义了一个Cache结构体,其中使用sync.Map作为底层数据结构来存储缓存项。Set方法用于设置缓存项,指定键、值以及过期时间。Get方法用于获取缓存项,如果缓存项存在且未过期,则返回值和true,否则返回nil和false。方法的接受者为指针类型,是为了对Cache对象进行操作,并在方法内部访问和......