go sync包(二) 互斥锁(二)

2024-06-21 02:20
文章标签 互斥 go sync

本文主要是介绍go sync包(二) 互斥锁(二),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

互斥锁 Mutex

mutex 的 加解锁很简单:

	var mutex sync.Mutexmutex.Lock()defer mutex.Unlock()// 加锁期间的代码逻辑

加锁

// Lock locks m.
// If the lock is already in use, the calling goroutine
// blocks until the mutex is available.
func (m *Mutex) Lock() {// Fast path: grab unlocked mutex.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()
}
  • 当我们调用 Lock 方法的时候,会先尝试走 Fast Path,也就是如果当前互斥锁如果处于未加锁的状态,尝试加锁,只要加锁成功就直接返回。
  • 否则的话就进入 slow path。
func (m *Mutex) lockSlow() {var waitStartTime int64 // 等待时间starving := false // 是否处于饥饿状态awoke := false // 是否处于唤醒状态iter := 0 // 自旋迭代次数old := m.state for {// Don't spin in starvation mode, ownership is handed off to waiters// so we won't be able to acquire the mutex anyway.// 判断当前 Goroutine 能否进入自旋// 条件:// 当前处于普通模式 && runtime_canSpin 返回 true// runtime_canSpin 返回 true//     1. 运行在多 CPU 的机器上//     2. 自旋次数不超过 4 次//     3. 当前机器上至少存在一个正在运行的处理器 P 并且处理的运行队列为空if old&(mutexLocked|mutexStarving) == mutexLocked && runtime_canSpin(iter) {// Active spinning makes sense.// Try to set mutexWoken flag to inform Unlock// to not wake other blocked goroutines.// 尝试设置 mutexWoken 状态,避免唤醒其他休眠的 goroutineif !awoke && old&mutexWoken == 0 && old>>mutexWaiterShift != 0 &&atomic.CompareAndSwapInt32(&m.state, old, old|mutexWoken) {awoke = true}// 自旋:执行 30 次 PAUSE指令,占用CPU并消耗CPU时间runtime_doSpin()iter++old = m.statecontinue}// 计算互斥锁的最新状态new := old// Don't try to acquire starving mutex, new arriving goroutines must queue.if old&mutexStarving == 0 {new |= mutexLocked}// 饥饿模式 || 锁已经被其他goroutine获取// 加入等待队列if old&(mutexLocked|mutexStarving) != 0 {new += 1 << mutexWaiterShift}// The current goroutine switches mutex to starvation mode.// But if the mutex is currently unlocked, don't do the switch.// Unlock expects that starving mutex has waiters, which will not// be true in this case.if starving && old&mutexLocked != 0 {new |= mutexStarving}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 会获得互斥锁,如果等待队列中只存在当前 Goroutine,// 互斥锁还会从饥饿模式中退出。if atomic.CompareAndSwapInt32(&m.state, old, new) {if old&(mutexLocked|mutexStarving) == 0 {break // locked the mutex with CAS}// If we were already waiting before, queue at the front of the queue.// 正在等,排在最前面queueLifo := waitStartTime != 0// 设置初始化时间,计算是否超过时间要切换到公平模式if waitStartTime == 0 {waitStartTime = runtime_nanotime()}// 阻塞runtime_SemacquireMutex(&m.sema, queueLifo, 1)// 看是否超过 1ms,是的话就切换到公平模式starving = starving || runtime_nanotime()-waitStartTime > starvationThresholdNsold = 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")}// 退出饥饿模式delta := int32(mutexLocked - 1<<mutexWaiterShift)if !starving || old>>mutexWaiterShift == 1 {// Exit starvation mode.// Critical to do it here and consider wait time.// Starvation mode is so inefficient, that two goroutines// can go lock-step infinitely once they switch mutex// to starvation mode.delta -= mutexStarving}atomic.AddInt32(&m.state, delta)break}awoke = trueiter = 0} else {old = m.state}}if race.Enabled {race.Acquire(unsafe.Pointer(m))}
}
  1. 判断当前 goroutine 能否可以进入自旋状态,可以的话自旋争抢锁。
    进入自旋状态的条件:
    • 普通模式
    • 运行在多 CPU 的机器上
    • 自旋次数不超过 4 次
    • 当前机器上至少存在一个正在运行的处理器 P 并且处理的运行队列为空
  2. 普通模式:被唤醒的 goroutine 跟新到来的 goroutine 争抢锁。
    饥饿模式:新到来的 goroutine 自动加入队列末尾,由队列第一个 goroutine 获得锁。
  3. 饥饿模式:
    进入条件:如果当前 goroutine 超过 1ms 都没有获取到锁就会进饥饿模式。
    退出条件:当前 goroutine 是队列中最后一个 goroutine。

解锁

// Unlock unlocks m.
// It is a run-time error if m is not locked on entry to Unlock.
//
// A locked Mutex is not associated with a particular goroutine.
// It is allowed for one goroutine to lock a Mutex and then
// arrange for another goroutine to unlock it.
func (m *Mutex) Unlock() {if race.Enabled {_ = m.staterace.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)}
}
  • 如果该函数返回的新状态等于 0,当前 Goroutine 就成功解锁了互斥锁。
  • 如果该函数返回的新状态不等于 0,这段代码会调用 sync.Mutex.unlockSlow 开始慢速解锁。
func (m *Mutex) unlockSlow(new int32) {// 校验锁状态的合法性// 如果当前互斥锁已经被解锁过,直接抛出异常中止当前程序if (new+mutexLocked)&mutexLocked == 0 {fatal("sync: unlock of unlocked mutex")}// 普通模式if new&mutexStarving == 0 {old := newfor {// If there are no waiters or a goroutine has already// been woken or grabbed the lock, no need to wake anyone.// In starvation mode ownership is directly handed off from unlocking// goroutine to the next waiter. We are not part of this chain,// since we did not observe mutexStarving when we unlocked the mutex above.// So get off the way.// // 没有等待者 || 已经被加锁 || 已经被解锁 || 公平锁if old>>mutexWaiterShift == 0 || old&(mutexLocked|mutexWoken|mutexStarving) != 0 {return}// Grab the right to wake someone.// 唤醒一个等待者new = (old - 1<<mutexWaiterShift) | mutexWokenif atomic.CompareAndSwapInt32(&m.state, old, new) {runtime_Semrelease(&m.sema, false, 1)return}old = m.state}} else {// Starving mode: handoff mutex ownership to the next waiter, and yield// our time slice so that the next waiter can start to run immediately.// Note: mutexLocked is not set, the waiter will set it after wakeup.// But mutex is still considered locked if mutexStarving is set,// so new coming goroutines won't acquire it.// 饥饿模式// 将当前锁让给下一个等待者// 这里不会解除饥饿模式,所以新来的goroutine不会获得锁runtime_Semrelease(&m.sema, true, 1)}
}// Semrelease atomically increments *s and notifies a waiting goroutine
// if one is blocked in Semacquire.
// It is intended as a simple wakeup primitive for use by the synchronization
// library and should not be used directly.
// If handoff is true, pass count directly to the first waiter.
// skipframes is the number of frames to omit during tracing, counting from
// runtime_Semrelease's caller.// hadoff:
// true: 唤醒并直接移交给第一个等待者
// false: 只是唤醒操作
func runtime_Semrelease(s *uint32, handoff bool, skipframes int)

小结

互斥锁的加锁过程比较复杂,它涉及自旋、信号量以及调度等概念:

  • 如果互斥锁处于初始化状态,会通过置位 mutexLocked 加锁。
  • 如果互斥锁处于 mutexLocked 状态并且在普通模式下工作,会进入自旋,执行 30 次 PAUSE 指令消耗 CPU 时间等待锁的释放。
  • 如果当前 Goroutine 等待锁的时间超过了 1ms,互斥锁就会切换到饥饿模式。
  • 互斥锁在正常情况下会通过 runtime.sync_runtime_SemacquireMutex 将尝试获取锁的 Goroutine 切换至休眠状态,等待锁的持有者唤醒(阻塞)。
  • 如果当前 Goroutine 是互斥锁上的最后一个等待的协程,那么它会将互斥锁切换回正常模式。

互斥锁的解锁过程比较简单:

  • 当互斥锁已经被解锁时,调用 sync.Mutex.Unlock 会直接抛出异常。
  • 当互斥锁处于饥饿模式时,将锁的所有权交给队列中的下一个等待者,等待者会负责设置 mutexLocked 标志位。
  • 当互斥锁处于普通模式时,如果没有 Goroutine 等待锁的释放或者已经有被唤醒的 Goroutine 获得了锁,会直接返回。在其他情况下会通过 sync.runtime_Semrelease 唤醒对应的 Goroutine。

这篇关于go sync包(二) 互斥锁(二)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1079942

相关文章

线程封装,互斥

文章目录 线程封装线程互斥加锁、解锁认识接口解决问题理解锁 线程封装 C/C++代码混编引起的问题 此处pthread_create函数要求传入参数为void * func(void * )类型,按理来说ThreadRoutine满足,但是 这是在内类完成封装,所以ThreadRoutine函数实际是两个参数,第一个参数Thread* this不显示 解决方法: 第

Go 三色标记法:一种高效的垃圾回收策略

💝💝💝欢迎莅临我的博客,很高兴能够在这里和您见面!希望您在这里可以感受到一份轻松愉快的氛围,不仅可以获得有趣的内容和知识,也可以畅所欲言、分享您的想法和见解。 推荐:「stormsha的主页」👈,持续学习,不断总结,共同进步,为了踏实,做好当下事儿~ 专栏导航 Python系列: Python面试题合集,剑指大厂Git系列: Git操作技巧GO系列: 记录博主学习GO语言的笔

Go语言中的go.mod与go.sum

问题1:什么是go.mod以及它是用来解决什么问题的? go mod 是 Go 语言引入的包管理工具,用于解决 Go 语言项目在依赖管理方面的问题。 传统上,若不使用go mod,则需要要通过GOPATH来管理依赖,而这种方式存在一些问题: 如1. 包管理依赖不明确 2. 依赖库的版本管理 3. 需要手动管理同步依赖的复杂性等 而go mod可以帮助开发者在项目中明确管理依赖的版

线程间通信方式(互斥(互斥锁)与同步(无名信号量、条件变量))

1通信机制:互斥与同步 线程的互斥通过线程的互斥锁完成; 线程的同步通过无名信号量或者条件变量完成。 2  互斥 2.1 何为互斥?         互斥是在多个线程在访问同一个全局变量的时候,先让这个线程争抢锁的资源,那个线程争抢到资源,它可以访问这个变量,没有争抢到资源的线程不能够访问这个变量。那这种只有一个线程能够访问到这个变量的现象称之为线程间互斥。 2.2互斥锁API 1.

client-go删除job同时删除job关联的pod

问题描述 client-go使用以下方式删除job时,并不会把其关联的pod删除,从而导致这些pod成为了孤儿(orphan): err := clientSet.BatchV1().Jobs(namespace).Delete(name, &metav1.DeleteOptions{}) 在删除job的时候将job关联的pod也删除的方法: propagationPolicy := m

client-go入门之1:创建连接Kubernetes集群的客户端

文章目录 简介使用 简介 我们可以使用Dashboard或kubectl来访问k8s的API,也可以使用编程语言,如Go,Java,Python作为客户端来访问k8s。client-go是一个使用go语言编写的库,用来连接k8s集群并对集群资源进行操作。 使用 以下代码使用go连上k8s集群,并查询集群的节点信息: package mainimport ("fmt"meta

【经典算法】LeetCode 22括号生成(Java/C/Python3/Go实现含注释说明,中等)

作者主页: 🔗进朱者赤的博客 精选专栏:🔗经典算法 作者简介:阿里非典型程序员一枚 ,记录在大厂的打怪升级之路。 一起学习Java、大数据、数据结构算法(公众号同名) ❤️觉得文章还不错的话欢迎大家点赞👍➕收藏⭐️➕评论,💬支持博主,记得点个大大的关注,持续更新🤞 ————————————————- 首先,请注意题目链接有误,您提供的链接是LeetCode 14,但题目

深入理解go语言反射机制

1、前言        每当我们学习一个新的知识点时,一般来说,最关心两件事,一是该知识点的用法,另外就是使用场景。go反射机制作为go语言特性中一个比较高级的功能,我们也需要从上面两个方面去进行学习,前者告诉我们如何去使用,而后者告诉我们为什么以及什么时候使用。 2、反射机制 2.1 反射机制的基本概念         在 Go 语言中,反射机制允许程序在运行时获取对象的类型信息、访问对

go语言:数据库sql查询保存任意数量字段的数据

1、查询任意列数的表,并输出 func search() {rows, _ := db.Query("select * from users") // 查询数据columns, _ := rows.Columns() // 查询到的字段名列表values := make([]any, len(columns)) // 根据字段数量,创建接收字段值的列表f

Go 语言学习笔记之数组与切片

大家好,我是码农先森。 数组与切片的区别 在 Go 语言中,数组和切片是两种不同的数据结构,它们之间有以下主要区别。 参数长度: 数组(Array):数组的长度是固定的,在创建时就需要指定数组的长度,无法动态改变;只有长度信息,通过 len() 函数获取。 切片(Slice):切片是对数组的一个引用,底层使用的是数组的数据结构,具有动态长度,可以动态增加或减少元素,实现动态扩容;有长度和容量