Golang中sync.Mutex的源码分析

 更新时间:2023年03月15日 09:42:16   作者:绵羊的微笑  
这篇文章将带大家从源码分析一下Golang中sync.Mutex的使用,文中的示例代码讲解详细,对我们学习Golang有一定的帮助,需要的可以参考一下

Mutex结构

type Mutex struct {
	state int32
	sema  uint32
}
  • state 记录锁的状态,转换为二进制前29位表示等待锁的goroutine数量,后三位从左到右分别表示当前g 是否已获得锁、是否被唤醒、是否正饥饿
  • sema 充当临界资源,其地址作为这个锁在全局的唯一标识,所有等待这个锁的goroutine都会在阻塞前把自己的sudog放到这个锁的等待队列上,然后等待被唤醒,sema的值就是可以被唤醒的goroutine的数目,只有0和1。

常量

const (
	mutexLocked = 1 << iota // mutex is locked  //值1,转二进制后三位为001,表示锁已被抢
	mutexWoken                                  //值2,转二进制后三位为010,告诉即将释放锁的g现在已有g被唤醒
	mutexStarving                               //值4,转二进制后三位为100,表示当前处在饥饿状态
	mutexWaiterShift = iota                     //值3,表示mutex.state右移3位为等待锁的goroutine数量
	starvationThresholdNs = 1e6                 //表示mutext切换到饥饿状态所需等待时间的阈值,1ms。
)

Locker接口

type Locker interface {
	Lock()
	Unlock()
}

下面重点看这两个方法。

加锁Lock

Lock()

func (m *Mutex) Lock() {
	// 第一种情况:快上锁,即此刻无人来抢锁
	if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {
		if race.Enabled {  //竞争检测相关,不用看
			race.Acquire(unsafe.Pointer(m))
		}
		return
	}
	// 第二种情况:慢上锁,即此刻有竞争对手
	m.lockSlow()
}

CompareAndSwapInt32(addr *int32, old, new int32) (swapped bool){},go的CAS操作,底层通过调用cpu指令集提供的CAS指令实现,位置在src/runtime/internal/atomic/atomic_amd64.s/·Cas(SB)。

  • 参数addr:变量地址
  • 参数old:旧值
  • 参数new:新值
  • 原理:如果addr和old相等,则将new赋值给addr,并且返回true,否则返回false

lockSlow()

// 注释里的第一人称“我”只当前g
func (m *Mutex) lockSlow() {
	var waitStartTime int64	// 等待开始的时间
	starving := false		// 我是否饥饿
	awoke := false			// 我是否被唤醒
	iter := 0				// 我的自旋次数
	old := m.state			// 这个锁此时此刻所有的信息
	for {
		// 如果:锁已经被抢了 或着 正处在饥饿状态 或者 允许我自旋 那么进行自旋
		if old&(mutexLocked|mutexStarving) == mutexLocked && runtime_canSpin(iter) {
			// 如果:我没有处在唤醒态 并且 当前无g处在唤醒态 并且
            // 有等待锁的g 并且CAS尝试将我置为唤醒态成功 则进行自旋
            // 之所以将我置为唤醒态是为了明示那些执行完毕正在退出的g不用再去唤醒其它g了,因为只允许存在一个唤醒的g。
			if !awoke && old&mutexWoken == 0 && old>>mutexWaiterShift != 0 &&
				atomic.CompareAndSwapInt32(&m.state, old, old|mutexWoken) {
				awoke = true
			}
			runtime_doSpin()	// 我自旋一次
			iter++				// 我自旋次数加1
			old = m.state
			continue
		}
		new := old	// new只是个中间态,后面的cas操作将会判断是否将这个中间态落实
		// 如果不是处在饥饿模式就立即抢锁
		if old&mutexStarving == 0 {
			new |= mutexLocked
		}
        // 如果锁被抢了 或者 处在饥饿模式,那就去排队
		if old&(mutexLocked|mutexStarving) != 0 {
			new += 1 << mutexWaiterShift	// 等待锁的goroutine数量加1
		}
		// 如果我现在饥渴难耐 而且 锁也被抢走了,那就立即将锁置为饥饿模式
		if starving && old&mutexLocked != 0 {
			new |= mutexStarving
		}
		if awoke {
			if new&mutexWoken == 0 {
				throw("sync: inconsistent mutex state")
			}
            // 释放我的唤醒态
            // 因为后面我要么抢到锁要么被阻塞,都不是处在和唤醒态
			new &^= mutexWoken
		}
        //此处CAS操作尝试将new这个中间态落实
		if atomic.CompareAndSwapInt32(&m.state, old, new) {
			if old&(mutexLocked|mutexStarving) == 0 {
				break // 抢锁成功!
			}
			// queueLifo我之前有没有排过队
			queueLifo := waitStartTime != 0
			if waitStartTime == 0 {
				waitStartTime = runtime_nanotime()
			}
            //原语:如果我之前排过队,这次就把我放到等待队列队首,否则把我放到队尾,并将我挂起
			runtime_SemacquireMutex(&m.sema, queueLifo, 1) 
            // 刚被唤醒的我先判断自己是不是饥饿了,如果我等待锁的时间小于starvationThresholdNs(1ms),那就不饿
			starving = starving || runtime_nanotime()-waitStartTime > starvationThresholdNs
			old = m.state
			if old&mutexStarving != 0 {
				// 我一觉醒来发觉锁正处在饥饿状态,苍天有眼这个锁属于我了,因为饥饿状态绝对没有人跟我抢锁
				if old&(mutexLocked|mutexWoken) != 0 || old>>mutexWaiterShift == 0 {
					throw("sync: inconsistent mutex state")
				}
                // delta是一个中间状态,atomic.AddInt32方法将给锁落实这个状态
				delta := int32(mutexLocked - 1<<mutexWaiterShift)
				if !starving || old>>mutexWaiterShift == 1 {
                    // 如果现在我不饥饿或者等待锁的就我一个,那么就将锁切换到正常状态。
                    // 饥饿模式效率很低,而且一旦有两个g把mutex切换为饥饿模式,那就会死锁。
					delta -= mutexStarving
				}
                // 原语:给锁落实delta的状态。
				atomic.AddInt32(&m.state, delta)
                // 我拿到锁啦
				break
			}
            // 把我的状态置为唤醒,我将继续去抢锁
			awoke = true
            // 把我的自旋次数置0,我又可以自旋抢锁啦
			iter = 0
		} else {
            // 继续去抢锁
			old = m.state
		}
	}

    // 竞争检测的代码,不管
	if race.Enabled {
		race.Acquire(unsafe.Pointer(m))
	}
}

runtime_canSpin(iter) 判断当前g可否自旋,已经自旋过iter次

func sync_runtime_canSpin(i int) bool {
	// 可自旋的条件:
    // 1.多核cpu
    // 2.GOMAXPROCS > 1 且 至少有一个其他的p在运行 且 该p的本地runq为空
    // 3.iter小于最大自旋次数active_spin = 4
	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(n int32)方法来实现空耗CPU,n乃空耗CPU的次数。

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

procyield(active_spin_cnt) 的底层通过执行PAUSE指令来空耗30个CPU时钟周期。

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

runtime_SemacquireMutex(&m.sema, queueLifo, 1) 将当前g放到mutex的等待队列中去

//go:linkname sync_runtime_SemacquireMutex sync.runtime_SemacquireMutex
func sync_runtime_SemacquireMutex(addr *uint32, lifo bool, skipframes int) {
	semacquire1(addr, lifo, semaBlockProfile|semaMutexProfile, skipframes)
}

semacquire1(addr *uint32, lifo bool, profile semaProfileFlags, skipframes int) 若lifo为true,则把g放到等待队列队首,若lifo为false,则把g放到队尾

atomic.AddInt32(int32_t *val, int32_t delta) 原语:给t加上t_delta

uint32_t
AddUint32 (uint32_t *val, uint32_t delta)
{
  return __atomic_add_fetch (val, delta, __ATOMIC_SEQ_CST);
}

解锁Unlock

Unlock

func (m *Mutex) Unlock() {
	if race.Enabled {
		_ = m.state
		race.Release(unsafe.Pointer(m))
	}

	// 如果没有g在等待锁则立即释放锁
	new := atomic.AddInt32(&m.state, -mutexLocked)
	if new != 0 {
		// 如果还有g在等待锁,则在锁释放后需要做一点收尾工作。
		m.unlockSlow(new)
	}
}

unlockSlow

func (m *Mutex) unlockSlow(new int32) {
	if (new+mutexLocked)&mutexLocked == 0 {
		throw("sync: unlock of unlocked mutex")
	}
    // 如果锁处在正常模式下
	if new&mutexStarving == 0 {
		old := new
		for {
			// 如果锁正处在正常模式下,同时 没有等待锁的g 或者 已经有g被唤醒了 或者 锁已经被抢了,就什么也不用做直接返回
			// 如果锁正处在饥饿模式下,也是什么也不用做直接返回
			if old>>mutexWaiterShift == 0 || old&(mutexLocked|mutexWoken|mutexStarving) != 0 {
				return
			}
			// 给锁的唤醒标志位置1,表示已经有g被唤醒了,Mutex.state后三位010
			new = (old - 1<<mutexWaiterShift) | mutexWoken
			if atomic.CompareAndSwapInt32(&m.state, old, new) {
                // 唤醒锁的等待队列头部的一个g
                // 并把g放到p的funq尾部
				runtime_Semrelease(&m.sema, false, 1)
				return
			}
			old = m.state
		}
	} else {
		// 锁处在饥饿模式下,直接唤醒锁的等待队列头部的一个g
		//因为在饥饿模式下没人跟刚被唤醒的g抢锁,所以不用设置锁的唤醒标志位
		runtime_Semrelease(&m.sema, true, 1)
	}
}

runtime_Semrelease(&m.sema, false, 1) 用来释放mutex等待队列上的一个g

//go:linkname sync_runtime_Semrelease sync.runtime_Semrelease
func sync_runtime_Semrelease(addr *uint32, handoff bool, skipframes int) {
	semrelease1(addr, handoff, skipframes)
}

semrelease1(addr, handoff, skipframes) 参数handoff若为true,则让被唤醒的g立刻继承当前g的时间片继续执行。若handoff为false,则把刚被唤醒的g放到当前p的runq中。

到此这篇关于Golang中sync.Mutex的源码分析 的文章就介绍到这了,更多相关Golang sync.Mutex内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • Golang 编写Tcp服务器的解决方案

    Golang 编写Tcp服务器的解决方案

    Golang 作为广泛用于服务端和云计算领域的编程语言,tcp socket 是其中至关重要的功能,这篇文章给大家介绍Golang 开发 Tcp 服务器及拆包粘包、优雅关闭的解决方案,感兴趣的朋友一起看看吧
    2022-10-10
  • Go 语言结构体链表的基本操作

    Go 语言结构体链表的基本操作

    链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的,这篇文章主要介绍了Go 语言结构体链表,需要的朋友可以参考下
    2022-04-04
  • goland服务热重启的配置文件

    goland服务热重启的配置文件

    这篇文章主要介绍了goland服务热重启的配置文件,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2020-12-12
  • Go语言中转换JSON数据简单例子

    Go语言中转换JSON数据简单例子

    这篇文章主要介绍了Go语言中转换JSON数据简单例子,本文先定义了一个结构体,然后把JSON绑定到结构体上实现读取,需要的朋友可以参考下
    2014-10-10
  • Go 修改map slice array元素值操作

    Go 修改map slice array元素值操作

    这篇文章主要介绍了Go 修改map slice array元素值操作,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2020-12-12
  • prometheus client_go为应用程序自定义监控指标

    prometheus client_go为应用程序自定义监控指标

    这篇文章主要为大家介绍了prometheus client_go为应用程序自定义监控指标详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-02-02
  • Go语言method详解

    Go语言method详解

    这篇文章主要介绍了Go语言method详解,本文总结了在使用method的时候重要注意几点、指针作为receiver、method继承等内容,需要的朋友可以参考下
    2014-10-10
  • Go json反序列化“null“的问题解决

    Go json反序列化“null“的问题解决

    本文主要介绍了Go json反序列化“null“的问题解决,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2023-03-03
  • Golang的Fork/Join实现代码

    Golang的Fork/Join实现代码

    Fork/Join本质上是一种任务分解,将一个很大的任务分解成若干个小任务,然后再对小任务进一步分解,直到最小颗粒度,然后并发执行,对Golang的Fork/Join实现代码感兴趣的朋友跟随小编一起看看吧
    2023-01-01
  • Go error的使用方式详解

    Go error的使用方式详解

    当我们需要在Go项目中设计error,就不得不先知道Go error几种常用方法,今天通过本文给大家介绍Go error的使用方式详解,感兴趣的朋友一起看看吧
    2022-05-05

最新评论