广告
返回顶部
首页 > 资讯 > 后端开发 > GO >Go语言底层原理互斥锁的实现原理
  • 499
分享到

Go语言底层原理互斥锁的实现原理

2024-04-02 19:04:59 499人浏览 八月长安
摘要

目录Go 互斥锁的实现原理?概念使用场景底层实现结构操作加锁解锁Go 互斥锁正常模式和饥饿模式的区别?正常模式(非公平锁)饥饿模式(公平锁)Go 互斥锁允许自旋的条件?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)  
if !starving || old>>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 <= 1 
    || gomaxprocs <= int32(sched.npidle+sched.nmspinning)+1 {  
          return false  
     }  
   if p := getg().m.p.ptr(); !runqempty(p) {  
      return false  
 }  
   return true  
}

自旋:

func sync_runtime_doSpin() {
    procyield(active_spin_cnt)
}    

如果可以进入自旋状态之后就会调用 runtime_doSpin 方法进入自旋, doSpin 方法会调用 procyield(30) 执行30次 PAUSE 指令,什么都不做,但是会消耗CPU时间

到此这篇关于Go语言底层原理互斥锁的实现原理的文章就介绍到这了,更多相关Go 互斥锁内容请搜索编程网以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程网!

您可能感兴趣的文档:

--结束END--

本文标题: Go语言底层原理互斥锁的实现原理

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

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

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

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

下载Word文档
猜你喜欢
  • Go语言底层原理互斥锁的实现原理
    目录Go 互斥锁的实现原理?概念使用场景底层实现结构操作加锁解锁Go 互斥锁正常模式和饥饿模式的区别?正常模式(非公平锁)饥饿模式(公平锁)Go 互斥锁允许自旋的条件?Go 互斥锁的...
    99+
    2022-11-11
  • Go语言上下文context底层原理
    目录1. context 介绍2. 基本介绍3. 源码分析3.1 Context 接口3.2 emptyCtx3.3 cancelCtx3.4 timerCtx3.5 valueCt...
    99+
    2022-11-13
  • go语言中slice,map,channl底层原理
    目录0. 前序1. slice1.1 slice的创建1.2 数据结构1.3 扩容机制2. map2.1 map创建2.2 数据结构2.3 扩容机制3. channl3.1 数...
    99+
    2022-06-07
    map GO slice go语言
  • Go语言中的并发goroutine底层原理
    目录一、基本概念①并发、并行区分②从用户态线程,内核态线程阐述go与java并发的优劣②高并发为什么是Go语言强项?③Go语言实现高并发底层GMP模型原理解析二、上代码学会Go语言并...
    99+
    2022-11-13
  • Go语言原子操作及互斥锁的区别
    目录增或减比较并交换(Compare And Swap)载入与存储交换原子值原子操作与互斥锁的区别原子操作就是不可中断的操作,外界是看不到原子操作的中间状态,要么看到原子操作已...
    99+
    2022-06-07
    GO 互斥 原子 原子操作 互斥锁 go语言
  • Go WaitGroup及Cond底层实现原理
    目录WaitGroup概念底层数据结构使用方法Cond概念底层数据结构使用方法WaitGroup 概念 Go标准库提供了WaitGroup原语, 可以用它来等待一批 Goroutin...
    99+
    2022-11-11
  • Go语言CSP并发模型goroutine及channel底层实现原理
    目录Go的CSP并发模型(goroutine + channel)1、goroutinegoroutine的优点:2、channel无缓存channel有缓存channel3、Go并...
    99+
    2022-11-13
  • go语言中slice,map,channl底层原理是什么
    今天小编给大家分享一下go语言中slice,map,channl底层原理是什么的相关知识点,内容详细,逻辑清晰,相信大部分人都还太了解这方面的知识,所以分享这篇文章给大家参考一下,希望大家阅读完这篇文章后有所收获,下面我们一起来了解一下吧。...
    99+
    2023-06-30
  • Linux互斥锁的实现原理是什么
    本篇内容主要讲解“Linux互斥锁的实现原理是什么”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“Linux互斥锁的实现原理是什么”吧!互斥锁(Mutex)是在原子操作API的基础上实现的信号量行...
    99+
    2023-06-28
  • Go语言中并发goroutine底层原理的示例分析
    小编给大家分享一下Go语言中并发goroutine底层原理的示例分析,希望大家阅读完这篇文章之后都有所收获,下面让我们一起去探讨吧!一、基本概念①并发、并行区分1.概念并发:同一时间段内一个对象执行多个任务,充分利用时间并行:同一时刻,多个...
    99+
    2023-06-29
  • Javasynchronized底层的实现原理
    目录监视器底层实现执行流程总结前言: 想了解 synchronized 是如何运行的?就要先搞清楚 synchronized 是如何实现? synchronized 同步锁是通过 J...
    99+
    2022-11-13
  • HashMap的底层实现原理
    这篇文章主要介绍“HashMap的底层实现原理”,在日常操作中,相信很多人在HashMap的底层实现原理问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”HashMap的底层实现原理”的疑惑有所帮助!接下来,请跟...
    99+
    2023-06-04
  • 深入了解Go的interface{}底层原理实现
    目录1. interface{}初探2. eface3. iface4. 接口转化1. interface{}初探 Go是强类型语言,各个实例变量的类型信息正是存放在inter...
    99+
    2022-06-07
    GO interface
  • C语言函数调用底层实现原理分析
    目录前言寄存器分配寄存器使用约定栈帧结构堆栈操作函数调用流程函数调用常用命令函数调用约定常见调用约定cdecl调用约定stdcall调用约定(微软命名)fastcall调用约定thi...
    99+
    2023-02-24
    C语言函数调用 C语言函数 函数底层实现原理
  • Go语言原子操作及互斥锁的区别是什么
    本篇内容介绍了“Go语言原子操作及互斥锁的区别是什么”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!原子操作就是不可中断的操作,外界是看不到原...
    99+
    2023-06-22
  • Java synchronized底层实现原理以及锁优化
    目录一、概述synchronized简介synchronized作用synchronized的使用二、实现原理三、理解Java对象头四、JVM对synchronized的锁优化1、偏...
    99+
    2022-11-13
  • Go底层channel实现原理及示例详解
    目录概念:使用场景:底层数据结构:操作:创建发送接收关闭案例分析:概念: Go中的channel 是一个队列,遵循先进先出的原则,负责协程之间的通信(Go 语言提倡不要通过共享内存来...
    99+
    2022-11-11
  • C语言函数调用底层实现原理是什么
    本文小编为大家详细介绍“C语言函数调用底层实现原理是什么”,内容详细,步骤清晰,细节处理妥当,希望这篇“C语言函数调用底层实现原理是什么”文章能帮助大家解决疑惑,下面跟着小编的思路慢慢深入,一起来学习新知识吧。前言C语言程序执行实质上的函数...
    99+
    2023-07-05
  • java语言自行实现ULID过程底层原理详解
    目录前提ULID出现的背景ULID规范组成排序规范的表示形式编码单调性溢出错误处理二进制布局ULID使用实现ULIDULID性能评估小结前提 最近发现各个频道推荐了很多ULID相关文...
    99+
    2022-11-13
    java ULID底层原理 java ULID
  • Synchronized的底层实现原理(原理解析,面试必备)
    synchronized 一. synchronized解读 1.1 简单描述 synchronized关键字解决的是多个线程之间访问资源的同步性,synchronized 翻译为中文的意思是同步,也称之为同步锁。 synchronize...
    99+
    2023-08-19
    面试 java jvm
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作