iis服务器助手广告广告
返回顶部
首页 > 资讯 > 后端开发 > GO >GolangMutex实现互斥的具体方法
  • 420
分享到

GolangMutex实现互斥的具体方法

GolangMutex互斥GolangMutex 2023-05-17 11:05:11 420人浏览 八月长安
摘要

目录获取锁未锁——直接获取在不饥饿且旋的不多的情况下,尝试自旋自旋究竟在做什么呢?计算期望状态尝试达成获取锁期望考虑几种场景释放锁只有已锁—&md

Mutex是golang常见的并发原语,不仅在开发过程中经常使用到,如channel这种具有Golang特色的并发结构也依托于Mutex从而实现

type Mutex struct {
  // 互斥锁的状态,比如是否被锁定
  state int32
  // 表示信号量。堵塞的协程会等待该信号量,解锁的协程会释放该信号量
  sema int32
}
const (
  // 当前是否已经上锁
  mutexLocked = 1 << iota // 1
  // 当前是否有唤醒的goroutine
  mutexWoken // 2
  // 当前是否为饥饿状态
  mutexStarving // 4
  // state >> mutexWaiterShift 得到等待者数量
  mutexWaiterShift = iota // 3

  starvationThresholdNs = 1e6 // 判断是否要进入饥饿状态的阈值
)

Mutex有正常饥饿模式。

  • 正常模式:等待者会入队,但一个唤醒的等待者不能持有锁,以及与新到来的goroutine进行竞争。新来的goroutine有一个优势——他们已经运行在CPU上。
    超过1ms没有获取到锁,就会进入饥饿模式
  • 饥饿模式:锁的所有权直接移交给队列头goroutine,新来的goroutine不会尝试获取互斥锁,即使互斥锁看起来已经解锁,也不会尝试旋转。相反,他们自己排在等待队列的末尾。

若等待者是最后一个,或者等待小于1ms就会切换回正常模式

获取锁

未锁——直接获取

func (m *Mutex) Lock() {
    // 快路径。直接获取未锁的mutex
  // 初始状态为0,所以只要状态存在其他任何状态位都是无法直接获取的
    if atomic.CompareAndSwapint32(&m.state, 0, mutexLocked) {
        if race.Enabled {
            race.Acquire(unsafe.Pointer(m))
        }
        return
    }
    // Slow path (outlined so that the fast path can be inlined)
    m.lockSlow()
}

在不饥饿且旋的不多的情况下,尝试自旋

        // 只要原状态已锁且不处于饥饿状态,并满足自旋条件
        if old&(mutexLocked|mutexStarving) == mutexLocked && runtime_canSpin(iter) {
            // 在当前goroutine没有唤醒,且没有其他goroutine在尝试唤醒,且存在等待的情况下,cas标记存在goroutine正在尝试唤醒。若标记成功就设置当前goroutine已经唤醒了
            if !awoke && old&mutexWoken == 0 && old>>mutexWaiterShift != 0 &&
                atomic.CompareAndSwapInt32(&m.state, old, old|mutexWoken) {
                awoke = true
            }
      // 自旋
            runtime_doSpin()
      // 自旋次数加一
            iter++
      // 更新原状态
            old = m.state
            continue
        }

具体的自旋条件如下

  • 自旋次数小于4
  • 多核CPU
  • p数量大于1
  • 至少存在一个p的队列为空
const (
    locked uintptr = 1

    active_spin     = 4
    active_spin_cnt = 30
    passive_spin    = 1
)

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
}

自旋究竟在做什么呢?

自旋是由方法runtime_doSpin()执行的,实际调用了procyield()

# 定义了一个runtime.procyield的文本段,通过NOSPLIT不使用栈分裂,$0-0 表示该函数不需要任何输入参数和返回值
TEXT runtime·procyield(SB),NOSPLIT,$0-0
    # 从栈帧中读取cycles参赛值,并储存在寄存器R0中
    MOVWU    cycles+0(FP), R0
# 组成无限循环。在每次循环中,通过YIELD告诉CPU将当前线程置于休眠状态
# YIELD: x86上,实现为PAUSE指令,会暂停处理器执行,切换CPU到低功耗模式并等待更多数据到达。通常用于忙等待机制,避免无谓CPU占用
# ARM上,实现为WFE(Wait For Event),用于等待中断或者其他事件发生。在某些情况下可能会导致CPU陷入死循环,因此需要特殊处理逻辑解决
again:
    YIELD
    # 将R0值减1
    SUBW    $1, R0
    # CBNZ(Compare and Branch on Non-Zero)检查剩余的时钟周期数是否为0。不为0就跳转到标签again并再次调用YIELD,否则就退出函数
    CBNZ    R0, again
    RET

以上汇编代码分析过程感谢chatgpt的大力支持

从代码中可以看到自旋次数是30次

const active_spin_cnt = 30

func sync_runtime_doSpin() {
    procyield(active_spin_cnt)
}

计算期望状态

1.原状态不处于饥饿状态,新状态设置已锁状态位

原状态处于已锁状态或饥饿模式,新状态设置等待数量递增

当前goroutine是最新获取锁的goroutine,在正常模式下期望就是要获取锁,那么就应该设置新状态已锁状态位

如果锁已经被抢占了,或者处于饥饿模式,那么就应该去排队

2.若之前尝试获取时已经超过饥饿阈值时间,且原状态已锁,那么新状态设置饥饿状态位

3.若goroutine处于唤醒,则新状态清除正在唤醒状态位

期望是已经获取到锁了,那么自然要清除正在获取的状态位

        new := old
        // Don't try to acquire starving mutex, new arriving goroutines must queue.
    // 若原状态不处于饥饿状态,就给新状态设置已加锁
        if old&mutexStarving == 0 {
            new |= mutexLocked
        }
    // 只要原状态处于已锁或者饥饿模式,就将新状态等待数量递增
        if old&(mutexLocked|mutexStarving) != 0 {
            new += 1 << mutexWaiterShift
        }
    // 若已经等待超过饥饿阈值时间且原状态已锁,就设置新状态为饥饿
    // 这也意味着如果已经不处于已锁状态,就可以切换回正常模式了
        if starving && old&mutexLocked != 0 {
            new |= mutexStarving
        }
    // 如果已经唤醒了(也就是没有其他正在抢占的goroutine),则在新状态中清除正在唤醒状态位
        if awoke {
            // The goroutine has been woken from sleep,
            // so we need to reset the flag in either case.
            if new&mutexWoken == 0 {
                throw("sync: inconsistent mutex state")
            }
            new &^= mutexWoken
        }

尝试达成获取锁期望

cas尝试从原状态更新为新的期望状态

如果失败,则更新最新状态,继续尝试获取锁

说明这期间锁已经被抢占了

若原来既没有被锁住,也没有处于饥饿模式,那么就获取到锁,直接返回

排队。若之前已经在等待了就排到队列头

获取信号量。此处会堵塞等待

被唤醒,认定已经持有锁。并做以下饥饿相关处理

  • 计算等待时长,若超出饥饿阈值时间,就标记当前goroutine处于饥饿
  • 若锁处于饥饿模式,递减等待数量,并且在只有一个等待的时候,切换锁回正常模式
if atomic.CompareAndSwapInt32(&m.state, old, new) {
      // 如果原状态既没有处于已锁状态,也没有处于饥饿模式
      // 那么就表示已经获取到锁,直接退出
            if old&(mutexLocked|mutexStarving) == 0 {
                break // locked the mutex with CAS
            }
      // 若已经在等待了,就排到队列头
            queueLifo := waitStartTime != 0
            if waitStartTime == 0 {
                waitStartTime = runtime_nanotime()
            }
      // 尝试获取信号量。此处获取一个信号量以实现互斥
      // 此处会进行堵塞
            runtime_SemacquireMutex(&m.sema, queueLifo, 1)
      // 被信号量唤醒之后,发现若等待时间超过饥饿阈值,就切换到饥饿模式
            starving = starving || runtime_nanotime()-waitStartTime > starvationThresholdNs
            old = m.state
      // 处于饥饿模式下
            if old&mutexStarving != 0 {
                // If this goroutine was woken and mutex is in starvation mode,
                // ownership was handed off to us but mutex is in somewhat
                // inconsistent state: mutexLocked is not set and we are still
                // accounted as waiter. Fix that.
        // 若既没有已锁且正在尝试唤醒,或者等待队列为空,就代表产生了不一致的状态
                if old&(mutexLocked|mutexWoken) != 0 || old>>mutexWaiterShift == 0 {
                    throw("sync: inconsistent mutex state")
                }
        // 当前goroutine已经获取锁,等待队列减1;若等待者就一个,就切换正常模式。退出
                delta := int32(mutexLocked - 1<<mutexWaiterShift)
                if !starving || old>>mutexWaiterShift == 1 {
                    delta -= mutexStarving
                }
                atomic.AddInt32(&m.state, delta)
                break
            }
      // 不处于饥饿模式下,设置当前goroutine为唤醒状态,重置自璇次数,继续尝试获取锁
            awoke = true
            iter = 0
        } else {
      // 若锁被其他goroutine占用了,就更新原状态,继续尝试获取锁
            old = m.state
        }

考虑几种场景

  • 如果lock当前只有一个goroutine g1去获取锁,那么会直接快路径,cas更新已锁状态位,获取到锁
  • 如果锁已经被g1持有,
    • 此时g2会先自旋4次,
    • 然后计算期望状态为已锁、等待数量为1、唤醒状态位被清除
    • 在cas更新的时候尝试更新锁状态成功,接着因为原状态本身处于已锁,所以就不能获取到锁,只能排队,信号量堵塞
    • g1释放锁后,g2被唤醒,接着再次计算期望状态,并cas更新状态成功,直接获取到锁
  • 如果锁已经被g1持有,且g2在第一次尝试获取时超过了1ms(也就是饥饿阈值),那么
    • 计算期望状态为已锁、饥饿、清除唤醒状态位
    • cas更新状态成功,排在队列头,并被信号量堵塞
    • g1释放锁后,g2被唤醒就直接获取到锁,并减去排队数量以及清空饥饿位

释放锁

只有已锁——直接释放

如果没有排队的goroutine,没有处于饥饿状态,也没有真正尝试获取锁的goroutine,那么就可以直接cas更新状态为0

func (m *Mutex) Unlock() {
    // Fast path: drop lock bit.
    new := atomic.AddInt32(&m.state, -mutexLocked)
    if new != 0 {
        // Outlined slow path to allow inlining the fast path.
        // To hide unlockSlow during tracing we skip one extra frame when tracing GoUnblock.
        m.unlockSlow(new)
    }
}

慢释放

  • 如果原锁没有被锁住,就报错
  • 若原状态不处于饥饿状态,尝试唤醒等待者
    • 若现在锁已经被获取、正在获取、饥饿或者没有等待者,直接返回
    • 期望状态等待数量减1,并设置正在唤醒状态位
    • cas尝试更新期望状态,若成功,释放
    • 失败说明在这过程中又有goroutine在尝试获取,那么继续下一轮释放
  • 处于饥饿状态,直接释放信号量,移交锁所有权
func (m *Mutex) unlockSlow(new int32) {
  // 若原状态根本没有已锁状态位
    if (new+mutexLocked)&mutexLocked == 0 {
        throw("sync: unlock of unlocked mutex")
    }
  // 若原状态不处于饥饿状态
    if new&mutexStarving == 0 {
        old := new
        for {
      // 若没有等待,或者存在goroutine已经被唤醒,或者已经被锁住了,就不需要唤醒任何人,返回
            if old>>mutexWaiterShift == 0 || old&(mutexLocked|mutexWoken|mutexStarving) != 0 {
                return
            }
            // Grab the right to wake someone.
      // 设置期望状态为正在获取状态位,并减去一个等待者
            new = (old - 1<<mutexWaiterShift) | mutexWoken
      // 尝试cas更新为期望新状态,若成功就释放信号量,失败就更新原状态,进行下一轮释放
      // 失败说明在这过程中又有goroutine在尝试获取,比如已经获取到了、变成饥饿了、自旋等
            if atomic.CompareAndSwapInt32(&m.state, old, new) {
                runtime_Semrelease(&m.sema, false, 1)
                return
            }
            old = m.state
        }
    } else {
    // 饥饿模式下就移交锁所有权给下一个等待者,并放弃时间片,以便该等待者可以快速开始
        runtime_Semrelease(&m.sema, true, 1)
    }
}

到此这篇关于Golang Mutex实现互斥的具体方法的文章就介绍到这了,更多相关Golang Mutex互斥内容请搜索编程网以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程网!

您可能感兴趣的文档:

--结束END--

本文标题: GolangMutex实现互斥的具体方法

本文链接: https://www.lsjlt.com/news/211096.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

本篇文章演示代码以及资料文档资料下载

下载Word文档到电脑,方便收藏和打印~

下载Word文档
猜你喜欢
  • GolangMutex实现互斥的具体方法
    目录获取锁未锁——直接获取在不饥饿且旋的不多的情况下,尝试自旋自旋究竟在做什么呢?计算期望状态尝试达成获取锁期望考虑几种场景释放锁只有已锁—&md...
    99+
    2023-05-17
    Golang Mutex互斥 Golang Mutex
  • Go 互斥锁和读写互斥锁的实现
    目录互斥锁读写互斥锁 先来看这样一段代码,所存在的问题: var wg sync.WaitGroup var x int64 func main() { wg.Add(2)...
    99+
    2024-04-02
  • TypeScript中的互斥类型实现方法示例
    目录前言前置知识对象中多属性同类型的定义never类型剔除联合类型中的属性将对象中的所有属性转为联合类型实现互斥类型实现代码测试用例用例拆解写在最后前言 有这样一个对象,它有两个属性...
    99+
    2024-04-02
  • C++实现对象池的具体方法
    目录前言一、什么是对象池二、如何实现1.确定接口2.转成代码三、完整代码四、使用示例1、对象复用,示例:2、简易的线程池,示例:总结前言 需求无限,但资源有限的情况下,就需要对资源进...
    99+
    2024-04-02
  • Linux互斥锁的实现原理是什么
    本篇内容主要讲解“Linux互斥锁的实现原理是什么”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“Linux互斥锁的实现原理是什么”吧!互斥锁(Mutex)是在原子操作API的基础上实现的信号量行...
    99+
    2023-06-28
  • Opensuse中文设置的具体实现方法
    本篇内容主要讲解“Opensuse中文设置的具体实现方法”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“Opensuse中文设置的具体实现方法”吧!Opensuse中文设置的具体实现首先要安装相关...
    99+
    2023-06-16
  • C++实现WPF动画的具体操作方法
    本篇文章为大家展示了C++实现WPF动画的具体操作方法,内容简明扼要并且容易理解,绝对能使你眼前一亮,通过这篇文章的详细介绍希望你能有所收获。C++编程语言的应方式非常广泛,可以帮助我们轻松的实现许多功能需求。很多人都习惯使用Blend来帮...
    99+
    2023-06-17
  • C++ 函数模板的语法及具体实现方法?
    c++++函数模板允许使用泛型类型参数定义函数,使函数可以处理不同类型的数据。具体实现如下:语法:template 返回类型 函数名(输入参数列表){ // 函数体 }泛型类型参数 t...
    99+
    2024-04-15
    c++ 函数模板
  • Go语言底层原理互斥锁的实现原理
    目录Go 互斥锁的实现原理?概念使用场景底层实现结构操作加锁解锁Go 互斥锁正常模式和饥饿模式的区别?正常模式(非公平锁)饥饿模式(公平锁)Go 互斥锁允许自旋的条件?Go 互斥锁的...
    99+
    2024-04-02
  • c++ Bellman-Ford算法的具体实现
    Bellman-Ford算法用于解决有边数限制的最短路问题,且可以应对有负边权的图 其时间复杂度为O(nm),效率较低 代码实现: #include<iostream&g...
    99+
    2024-04-02
  • Docker容器互联互通的实现方法
    目录网络集群查看mynet网络查看centos01的容器信息test-network网卡下的centos01访问mynet网卡下的mynet-centos01、mynet-tomca...
    99+
    2022-11-13
    Docker 容器互联 Docker 容器联通
  • 怎么在java中实现多线程的互斥与同步
    这篇文章将为大家详细讲解有关怎么在java中实现多线程的互斥与同步,文章内容质量较高,因此小编分享给大家做个参考,希望大家阅读完这篇文章后对相关知识有一定的了解。一、线程互斥与同步互斥:指的是多个线程不能同时访问共享变量同步:指的是多个线程...
    99+
    2023-06-15
  • php实现ip白名单的具体方法和步骤
    随着互联网的发展,网络威胁不断增加,如何保证网站的安全性是每个网站开发者都需要思考的问题。其中,ip白名单技术是一种常见的安全保护机制,可以大大降低网站受到攻击的风险。本文将介绍php实现ip白名单的具体方法和步骤。一、什么是ip白名单ip...
    99+
    2023-05-14
  • GoStruct结构体的具体实现
    目录什么是结构体1. 基本实例化(方法1)2. new实例化(方法2)3. 键值对初始化(方法3 结构体能够使用指针就使用指针)结构体方法和接收者encoding-json包1. s...
    99+
    2023-03-15
    Go Struct结构体 Go Struct
  • CSS中的圆角,隔行,变色的具体实现方法
    本篇内容主要讲解“CSS中的圆角,隔行,变色的具体实现方法”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“CSS中的圆角,隔行,变色的具体实现方法”吧! ...
    99+
    2024-04-02
  • numpy.insert()的具体使用方法
    目录1. 参数说明2. 示例2.1. 插入一列,值为标量2.2. 插入一列,值为一维矩阵2.3. 插入多列,值为标量2.4. 输入为一维向量2.5. 输入为矩阵numpy.inser...
    99+
    2023-02-09
    numpy.insert()使用
  • Pythonlistsort方法的具体使用
    目录描述 语法 使用示例 1. 所有参数都省略 2. 指定key参数 3. 指定reverse参数 注意事项 1. sort函数会改变原列表顺序 2. 列表元素类型不一致 3. Py...
    99+
    2024-04-02
  • Python3re.search()方法的具体使用
    re.search()方法扫描整个字符串,并返回第一个成功的匹配。如果匹配失败,则返回None。 与re.match()方法不同,re.match()方法要求必须从字符串的开头进行匹...
    99+
    2024-04-02
  • phpMyAdmin的具体配置方法
    这篇文章主要讲解了“phpMyAdmin的具体配置方法”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“phpMyAdmin的具体配置方法”吧!$cfgServers 数组 从1.4.2版本开始...
    99+
    2023-06-17
  • 模板化编程的具体实现方式?
    模板化编程允许根据类型生成代码,提高可重用性和性能。它包括:在 c++++ 中使用模板指定类型参数,并通过实例化来生成代码。利用元编程在编译时操作类型信息,实现代码生成和静态分析等功能。...
    99+
    2024-05-08
    模板化编程 具体实现方式 c++
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作