go-sync-mutex

news2024/12/24 3:08:40

Sync

​ Go 语言作为一个原生支持用户态进程(Goroutine)的语言,当提到并发编程、多线程编程时,往往都离不开锁这一概念。锁是一种并发编程中的同步原语(Synchronization Primitives),它能保证多个 Goroutine 在访问同一片内存时不会出现竞争条件(Race condition)等问题。

通过atomic.CompareAndSwapInt32调用汇编CAS(compare and swap)指令的原子性来实现临界区的互斥访问,保证只有一个协程获取到锁

​ 当其中一个 goroutine 获得了这个锁,其他 goroutine 尝试获取这个锁时将会被阻塞,直到持有锁的 goroutine 释放锁为止。

​ Go 语言在 sync 包中提供了用于同步的一些基本原语,包括常见的 sync.Mutexsync.RWMutexsync.WaitGroupsync.Oncesync.Cond

!Mutex互斥锁

​ Go 语言的 sync.Mutex 由两个字段 statesema 组成。其中 state 表示当前互斥锁的状态,而 sema 是用于控制锁状态的信号量。

type Mutex struct {
    state int32
    sema uint32		// 指针地址 0xF,存着结构体的地址
}

Mutex.state

状态字段

int32类型的state代表:

  • locked: 锁状态 1被锁 0未被锁

  • woken:1是否有goroutine模式被唤醒,0未被唤醒

  • starving:1进入饥饿模式,0正常模式

  • 其他位:代表获取锁的等待队列中的协程数,state是int32类型,说明是32bit,其余位是32-3 bits,所以最大排队协程数就是2^(32-3)

锁模式

  • 正常模式:队头和新协程的抢占,未抢占到的扔到队尾
  • 饥饿模式:按顺序获取锁,不得插队,防止队尾一直阻塞等待
正常模式

在正常模式下获取锁:

  1. 多线程下竞争锁,获取成功返回,修改sync.Mutex结构体字段。获取失败,自旋等待其他线程释放锁,4次之后仍然拿不到锁,goroutine加入到等待队列尾部,状态改成_GWaiting
  2. 获取到锁的线程释放锁,从等待队列头部唤醒一个Goroutine,状态改成_Grunning,他会和新创建并且获取锁的新goroutine(M正在运行的g_Grunning)争抢锁。
    1. 如果被唤醒的G仍然未能抢到锁,goroutine加入到等待队列头部,状态改成_GWaiting
    2. 如果被唤醒的G抢到锁,新创建的G相当于重新进入1步骤
饥饿模式

在饥饿模式下获取锁:

互斥锁会直接交给等待队列最前面的 Goroutine。新的 Goroutine 在该状态下不能获取锁、也不会进入自旋状态,它们只会在队列的末尾等待。如果一个 Goroutine 获得了互斥锁并且它在队列的末尾或者它等待的时间少于 1ms,那么当前的互斥锁就会切换回正常模式。

锁模型切换
  • 正常模式切换到饥饿模式:被唤醒的 Goroutine 超过 1ms 没有获取到锁,它就会将当前互斥锁切换饥饿模式,防止部分 Goroutine 被『饿死』。
  • 饥饿模式换到正常模式切:
    • 一个 Goroutine 获得了互斥锁并且它在队列的末尾,说明没有协程在竞争了,切换到正常模式
    • 被唤醒的 Goroutine 获得锁没超过 1ms ,切换到正常模式

Mutex.Sema

控制锁状态的信号量(互斥信号量)

// runtime/sema.go
type semaRoot struct {
	lock  mutex
	treap *sudog // 锁抢占者的 平衡树的根
	nwait uint32 // 抢占锁的goroutine的数量
}

互斥锁加锁/解锁

  • func (m *Mutex) Lock():Lock方法锁住m,如果m已经加锁,则阻塞直到m解锁。
func (m *Mutex) Lock() {
	// 未锁状态,获取锁return
	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()
}

func (m *Mutex) lockSlow() {
	var waitStartTime int64	// 协程抢占锁时间,时间超出,锁变成饥饿模式
	starving := false
	awoke := false
	iter := 0
	old := m.state
	for {
		// 锁住状态下 and 不是饥模式 and 在可自旋次数下 进入
		if old&(mutexLocked|mutexStarving) == mutexLocked && runtime_canSpin(iter) {
			// awoke标记是false and 锁非唤醒状态 and 锁的等待者大于0  
            // 满足这些条件,把锁变成唤醒状态
            // awoke flag标记成true
			if !awoke && old&mutexWoken == 0 && old>>mutexWaiterShift != 0 &&
				atomic.CompareAndSwapInt32(&m.state, old, old|mutexWoken) {
				awoke = true
			}
            // 自旋 汇编
			runtime_doSpin()
            // 累计自选次数
			iter++
            
            // 把唤醒状态 覆盖 old
			old = m.state
           
			continue
		}
        
        // 可能其他协程更改了锁状态:改成了`未锁住状态` 
        // 以下操作就有AB两种情况
        // A情况: 锁住状态 且 饥饿模式 (自旋次数超过4次)
        // B情况: 未锁住
        
        //拿到最新锁状态
		new := old

		// old不是饥饿模式(排除A情况),那是B情况,把new设置成锁状态
		if old&mutexStarving == 0 {
			new |= mutexLocked
		}
        
        // old 是 锁住状态 或 是饥饿模式。
        // 等待数+1 (当前协程加入等待)
		if old&(mutexLocked|mutexStarving) != 0 {
			new += 1 << mutexWaiterShift
		}
        
        // 饥饿标识非空 and old是锁住状态。 (第一次进入 且 A情况)
        // new设置成饥饿状态
		if starving && old&mutexLocked != 0 {
			new |= mutexStarving
		}
        
        // awoke标识是 唤醒状态
		if awoke {
			// new不是唤醒状态,锁标识不对,panic
			if new&mutexWoken == 0 {
				throw("sync: inconsistent mutex state")
			}
            // &^ 想异的位保留,相同的位清0。 非唤醒状态 变成 唤醒, 唤醒状态下变成非唤醒
			new &^= mutexWoken
		}
        
        // 此时new的3个字段状态 : 锁住,饥饿,唤醒状态未知
        // 如果状态没有被其他协程改变,状态更改成new
		if atomic.CompareAndSwapInt32(&m.state, old, new) {
            // 如果状态是非锁住 and 非饥饿模式 
            // compareAndSwapInt32已经改成锁住,break for
			if old&(mutexLocked|mutexStarving) == 0 {
				break // locked the mutex with CAS
			}
			
            // 设置排队者的开始等待时间
			queueLifo := waitStartTime != 0
			if waitStartTime == 0 {
				waitStartTime = runtime_nanotime()
			}
            
            // 信号量设置,阻塞等待(信号量的P操作,协程间通信)
			runtime_SemacquireMutex(&m.sema, queueLifo, 1)
            
            // 标记 饥饿标识, 如果是饥饿标识是true 或者 大于饥饿阈值 
			starving = starving || runtime_nanotime()-waitStartTime > starvationThresholdNs
            
            // 获取最新锁状态,虽然前面compareAndSwap已经改成了m.state : 锁住,饥饿,唤醒状态未知。但是前面阻塞有可能其他协程更改了状态
			old = m.state
            
            // 锁是饥饿模式
			if old&mutexStarving != 0 {
				
                // 锁是 锁住状态 或者 唤醒状态 或者 等待者为0个时
                // 抛出
				if old&(mutexLocked|mutexWoken) != 0 || old>>mutexWaiterShift == 0 {
					throw("sync: inconsistent mutex state")
				}
                
                // 
				delta := int32(mutexLocked - 1<<mutexWaiterShift)
                
                // 非贪婪模式 或则 等待者为1时
				if !starving || old>>mutexWaiterShift == 1 {
					delta -= mutexStarving
				}
				atomic.AddInt32(&m.state, delta)
				break
			}
			awoke = true
			iter = 0
		} else {
			old = m.state
		}
	}

	if race.Enabled {
		race.Acquire(unsafe.Pointer(m))
	}
}
  • func (m *Mutex) Unlock():Unlock方法解锁m,如果m未加锁会导致运行时错误。锁和线程无关,可以由不同的线程加锁和解锁。
func (m *Mutex) Unlock() {
	if race.Enabled {
		_ = m.state
		race.Release(unsafe.Pointer(m))
	}

	// 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)
	}
}

func (m *Mutex) unlockSlow(new int32) {
	if (new+mutexLocked)&mutexLocked == 0 {
		throw("sync: unlock of unlocked mutex")
	}
	if new&mutexStarving == 0 {
		old := new
		for {
			if old>>mutexWaiterShift == 0 || old&(mutexLocked|mutexWoken|mutexStarving) != 0 {
				return
			}
			// Grab the right to wake someone.
			new = (old - 1<<mutexWaiterShift) | mutexWoken
			if atomic.CompareAndSwapInt32(&m.state, old, new) {
				runtime_Semrelease(&m.sema, false, 1)
				return
			}
			old = m.state
		}
	} else {
		// 信号量中的V操作
		runtime_Semrelease(&m.sema, true, 1)
	}
}

信号量:信号量有两种原子操作,他们必须成对出现
P操作:信号量 减1,当信号量 <0 ,表明资源被占用,进程阻塞。 当信号量>=0,表明资源被释放(可用),进程可继续执行
V操作:信号量加1,当信号量<=0时,代表有阻塞中进程。当信号量>0,表明没有阻塞中进程,无需操作
互斥信号量,默认值为1
————————————————
版权声明:本文为CSDN博主「我是你的小阿磊」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qiu18610714529/article/details/109062176

example

import "sync"

func main() {

	m := sync.Mutex{}
	go user1(&m)
	go user2(&m)

	signalChan := make(chan os.Signal, 1)
	signal.Notify(signalChan, os.Interrupt)
	select {
	case <-signalChan:
		fmt.Println("catch interrupt signal")
		break
	}
}

func printer(str string, m *sync.Mutex) {
	m.Lock()         //加锁
	defer m.Unlock() //解锁
	for _, ch := range str {
		fmt.Printf("%c", ch)
		time.Sleep(time.Millisecond * 1)
	}
}
func user1(m *sync.Mutex) {
	printer("hello ", m)
}
func user2(m *sync.Mutex) {
	printer("world", m)
}

//打印结果
worldhello 或者 helloworld: 两个单词是有序的,不像`heworllldo`两个协程同时打印,说明某个协程会在mutex.Lock()进行自旋等待获取锁

RWMutex读写互斥锁

读写互斥锁 sync.RWMutex 是细粒度的互斥锁,它不限制资源的并发读,但是读写、写写操作无法并行执行。

type RWMutex struct {
	w           Mutex  // held if there are pending writers
	writerSem   uint32 // semaphore for writers to wait for completing readers
	readerSem   uint32 // semaphore for readers to wait for completing writers
	readerCount int32  // number of pending readers
	readerWait  int32  // number of departing readers
}
  • w — 复用互斥锁提供的能力;
  • writerSemreaderSem — 分别用于写等待读和读等待写:
  • readerCount 存储了当前正在执行的读操作数量;
  • readerWait 表示当写操作被阻塞时等待的读操作个数;

加锁/解锁

  • func (rw *RWMutex) RLock() :读加锁,如果有写锁,则阻塞等待

    func (rw *RWMutex) RLock() {
    	if race.Enabled {
    		_ = rw.w.state
    		race.Disable()
    	}
    	if atomic.AddInt32(&rw.readerCount, 1) < 0 {
    		// 阻塞,等待信号量的v操作释放共享内存,才能获得执行权
    		runtime_SemacquireMutex(&rw.readerSem, false, 0)
    	}
    	if race.Enabled {
    		race.Enable()
    		race.Acquire(unsafe.Pointer(&rw.readerSem))
    	}
    }
    
  • func (rw *RWMutex) RUnlock():解读锁,

    func (rw *RWMutex) RUnlock() {
    	if race.Enabled {
    		_ = rw.w.state
    		race.ReleaseMerge(unsafe.Pointer(&rw.writerSem))
    		race.Disable()
    	}
    	if r := atomic.AddInt32(&rw.readerCount, -1); r < 0 {
    		// Outlined slow-path to allow the fast-path to be inlined
    		rw.rUnlockSlow(r)
    	}
    	if race.Enabled {
    		race.Enable()
    	}
    }
    
    func (rw *RWMutex) rUnlockSlow(r int32) {
    	if r+1 == 0 || r+1 == -rwmutexMaxReaders {
    		race.Enable()
    		throw("sync: RUnlock of unlocked RWMutex")
    	}
    	// A writer is pending.
    	if atomic.AddInt32(&rw.readerWait, -1) == 0 {
    		// The last reader unblocks the writer.
    		runtime_Semrelease(&rw.writerSem, false, 1)
    	}
    }
    
  • func (rw *RWMutex) Lock(): 写锁,如果有读写锁被占用,阻塞等待所有读写锁释放后才能获得

    • 其他 Goroutine 在获取写锁时会进入自旋或者休眠
    • 有其他 Goroutine 持有互斥锁的读锁该 Goroutine 会调用 runtime.sync_runtime_SemacquireMutex 进入休眠状态等待所有读锁所有者执行结束后释放 writerSem 信号量将当前协程唤醒;
func (rw *RWMutex) Lock() {
	if race.Enabled {
		_ = rw.w.state
		race.Disable()
	}
	// First, resolve competition with other writers.
	rw.w.Lock()
	// Announce to readers there is a pending writer.
	r := atomic.AddInt32(&rw.readerCount, -rwmutexMaxReaders) + rwmutexMaxReaders
	// Wait for active readers.
	if r != 0 && atomic.AddInt32(&rw.readerWait, r) != 0 {
		runtime_SemacquireMutex(&rw.writerSem, false, 0)
	}
	if race.Enabled {
		race.Enable()
		race.Acquire(unsafe.Pointer(&rw.readerSem))
		race.Acquire(unsafe.Pointer(&rw.writerSem))
	}
}

example

func RMutex() {
	ch := make(chan struct{})
	rw := &sync.RWMutex{}
	go func() {
		rw.RLock()
		time.Sleep(time.Second * 5)
		defer rw.RUnlock()
		fmt.Println("fun1")
	}()

	go func() {
		time.Sleep(time.Millisecond * 500)
		rw.Lock()
		defer rw.Unlock()
		fmt.Println("fun2")
		close(ch)
	}()

	<-ch
}

// 先打印出fun1 再打印fun2 代表了读写互斥

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1174338.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【CMake】15分钟带你入门CMake

博主未授权任何人或组织机构转载博主任何原创文章&#xff0c;感谢各位对原创的支持&#xff01; 博主链接 本人就职于国际知名终端厂商&#xff0c;负责modem芯片研发。 在5G早期负责终端数据业务层、核心网相关的开发工作&#xff0c;目前牵头6G算力网络技术标准研究。 博客…

[黑马程序员SpringBoot2]——运维实用篇

目录&#xff1a; 工程打包与运行打包插件Boot工程快速启动&#xff08;Linux版本&#xff09;临时属性配置文件4级分类自定义配置文件多环境开发(yaml版)多环境开发多文件版&#xff08;yaml版&#xff09;多环境开发多文件版&#xff08;properties版&#xff09;多环境分组…

基于AOSP源码Android-10.0.0_r41分支编译,framework开发,修改系统默认字体大小

文章目录 基于AOSP源码Android-10.0.0_r41分支编译&#xff0c;framework开发&#xff0c;修改系统默认字体大小 基于AOSP源码Android-10.0.0_r41分支编译&#xff0c;framework开发&#xff0c;修改系统默认字体大小 主要修改一个地方就行 代码源码路径 frameworks/base/co…

直流无刷电机(BLDC)六步换相驱动

直流无刷电机&#xff08;BLDC&#xff09;六步换相驱动 文章目录 直流无刷电机&#xff08;BLDC&#xff09;六步换相驱动1. 前言2. 六步换相原理3. 电角度与机械角度4. 动手实践4.1 霍尔输出表测量4.2 换向控制4.3 代码编写 5. 总结 1. 前言 直流无刷电机相对直流有刷电机具…

AOE性能调优问题案例

AOE&#xff08;Ascend Optimization Engine&#xff09;是一款自动性能调优工具&#xff0c;目的是为了充分利用有限的硬件资源&#xff0c;满足算子和整网的性能要求。 本期就分享几个关于AOE性能调优问题的典型案例&#xff0c;并给出原因分析及解决方法。 调优过程中进程…

Mysql之多表查询上篇

Mysql之多表查询上篇 多表查询什么是多表查询笛卡尔积(交叉连接)产生笛卡尔积的条件避免笛卡尔积的方法 多表查询的分类1.等值连接 VS 非等值连接等值连接非等值连接扩展1表的别名扩展2&#xff1a;连接多个表 2.自连接与非自连接扩展3&#xff1a;SQL语法标准 内连接SQL92语法…

【大模型应用开发教程】04_大模型开发整体流程 基于个人知识库的问答助手 项目流程架构解析

大模型开发整体流程 & 基于个人知识库的问答助手 项目流程架构解析 一、大模型开发整体流程1. 何为大模型开发定义核心点核心能力 2. 大模型开发的整体流程1. 设计2. 架构搭建3. Prompt Engineering4. 验证迭代5. 前后端搭建 二、项目流程简析步骤一&#xff1a;项目规划与…

TensorRT量化实战课YOLOv7量化:YOLOv7-PTQ量化(二)

目录 前言1. YOLOv7-PTQ量化流程2. 模型标定3. 敏感层分析 前言 手写 AI 推出的全新 TensorRT 模型量化实战课程&#xff0c;链接。记录下个人学习笔记&#xff0c;仅供自己参考。 该实战课程主要基于手写 AI 的 Latte 老师所出的 TensorRT下的模型量化&#xff0c;在其课程的基…

el-tree中展示项换行展示

文章目录 效果如下所示&#xff1a;没有换行展示的效果修改样式换行之后的展示效果 想要了解el-tree使用的详情往下看代码和数据如下所示Vue代码中可能使用到的数据如下Vue的代码如下&#xff1a;没有换行展示的效果换行之后的展示效果样式调试 效果如下所示&#xff1a; 没有…

仅以此文,纪念毕业一年后的日子

22年6月份从华农毕业了之后&#xff0c;拿到了好几份不错的offer&#xff0c;最后我进入了我学生时代十分憧憬的一家公司&#xff08;腾讯&#xff09;工作&#xff0c;加上实习的时间&#xff0c;已经在腾讯差不多工作了两年了。 从一开始实习的时候的懵懂学生气到现在的清醒…

Azure 机器学习 - 使用 ONNX 对来自 AutoML 的计算机视觉模型进行预测

目录 一、环境准备二、下载 ONNX 模型文件2.1 Azure 机器学习工作室2.2 Azure 机器学习 Python SDK2.3 生成模型进行批量评分多类图像分类 三、加载标签和 ONNX 模型文件四、获取 ONNX 模型的预期输入和输出详细信息ONNX 模型的预期输入和输出格式多类图像分类 多类图像分类输入…

【Liunx基础】之指令(一)

【Liunx基础】之指令&#xff08;一&#xff09; 1.ls指令2.pwd命令3.cd指令4.touch指令5.mkdir指令(重要)6.rmdir指令与rm指令&#xff08;重要&#xff09;7.man指令&#xff08;重要&#xff09;8.cp指令&#xff08;重要&#xff09; &#x1f4c3;博客主页&#xff1a; 小…

核电堆芯组件动态特性试验研究

u 核电试验概述 反应堆是核电事业的核心组成部分之一&#xff0c;堆内构件、堆芯燃料组件等部件在冷却剂流动冲击下&#xff0c;会诱发剧烈振动&#xff0c;导致堆芯内试验件流道不稳定。为了保障反应堆的安全运行&#xff0c;根据国家核安全法规规定&#xff0c;有必要对受冷…

单链表的应用(2)

环形链表的约瑟夫问题 编号为 1 到 n 的 n 个人围成一圈。从编号为 1 的人开始报数&#xff0c;报到 m 的人离开。 下一个人继续从 1 开始报数。 n-1 轮结束以后&#xff0c;只剩下一个人&#xff0c;问最后留下的这个人编号是多少&#xff1f; 利用链表实现 思路&#xff1…

基础Redis-结构与命令

结构与命令 1.基础-Redisa.Redis数据结构介绍b.Redis通用命令c.key的结构d.String类型e.Hash类型f.List类型g.Set类型h.SortedSet类型 1.基础-Redis a.Redis数据结构介绍 Redis是一个key-value的数据库&#xff0c;key一般是String类型&#xff0c;不过value的类型多种多样&a…

idea中配置spring boot单项目多端口启动

参照文章 https://zhuanlan.zhihu.com/p/610767685 项目配置如下 下面为 idea 2023&#xff0c;不同版本的设置有区别&#xff0c;但是没那么大&#xff0c;idea 2023默认使用新布局&#xff0c;切换为经典布局即可。 在项目根目录的.idea/workspace.xml文件里添加如下配置 &l…

Java用log4j写日志

日志可以方便追踪和调试问题&#xff0c;以前用log4net写日志&#xff0c;换Java了改用log4j写日志&#xff0c;用法和log4net差不多。 到apache包下载下载log4j的包&#xff0c;解压后把下图两个jar包引入工程 先到网站根下加一个log4j2.xml的配置文件来配置日志的格式和参…

测试开发面试宝典,涨价倒计时

大家好&#xff0c;我是洋子&#xff0c;相信在面试软件测试、测试开发岗位的小伙伴都深有体会&#xff0c;考察的知识点越来越多 不仅会考察到软件测试的理论&#xff0c;让你对某种功能进行测试用例的设计&#xff0c;更难一点会给出一个测试场景进行测试方案的设计&#xf…

C++二分算法:平衡子序列的最大和

涉及知识点 二分 动态规划 #题目 给你一个下标从 0 开始的整数数组 nums 。 nums 一个长度为 k 的 子序列 指的是选出 k 个 下标 i0 < i1 < … < ik-1 &#xff0c;如果这个子序列满足以下条件&#xff0c;我们说它是 平衡的 &#xff1a; 对于范围 [1, k - 1] 内的所…

openGauss学习笔记-115 openGauss 数据库管理-设置安全策略-设置密码安全策略

文章目录 openGauss学习笔记-115 openGauss 数据库管理-设置安全策略-设置密码安全策略115.1 操作步骤 openGauss学习笔记-115 openGauss 数据库管理-设置安全策略-设置密码安全策略 115.1 操作步骤 用户密码存储在系统表pg_authid中&#xff0c;为防止用户密码泄露&#xff…