go中mutex的sema信号量是什么?

先看下go的sync.mutex是什么

go 复制代码
type Mutex struct {
	state int32
	sema  uint32
}

这里面有个sema,这个就是信号量。

什么是信号量?

信号量通俗的来说是一个"变量"充当"信号(灯\旗)功能",信号量的值可以是任意非负整数,通常用于表示资源的可用数量。当一个(线程\协程)想要访问共享资源时,它必须先尝试将信号量的值减少1(原子操作),如果信号量的值变为负数,则(线程\协程)将被阻塞,直到信号量的值大于等于0。

当一个线程完成了对共享资源的访问,它需要将信号量的值增加1,以便其他(线程\协程)可以继续访问共享资源。如果有其他(线程\协程)正在等待信号量,则其中一个(线程\协程)将被唤醒,并被允许继续执行。

信号量的目的是确保对共享资源的有序访问,以避免竞争条件和数据不一致的情况发生。在并发编程中,信号量是一种常见的同步机制,可用于解决诸如互斥访问、缓冲区管理等问题。

sema在mutex里面是怎么实现的?

在go的sync.mutex里面 sema表面上来看就是个uint32,但是实际上他底层是一个semaRoot结构体:

go 复制代码
type semaRoot struct {
	lock  mutex
	treap *sudog        // root of balanced tree of unique waiters.
	nwait atomic.Uint32 // Number of waiters. Read w/o the lock.
}

semaRoot里面有三个成员,分别是:

1,lock,这是一个mutex类型,要注意的是,这不是sync包里面的mutex,这是runtime2包里面的mutex,关于runtime包里的mutex其实用的地方也非常多,之前写过一个 《go中runtime包里面的mutex是什么?runtime.mutex解析

这个lock锁主要的作用是保护semaRoot结构体的访问,防止多个goroutine竞争访问semaroot的时候出现并发问题。

2, treap实际上是一个平衡树(balanced tree)的root节点,他的主要作用其实就是存等待这个锁的goroutine,当一个g进来请求锁的时候,如果锁没有得到,就开始进入等待,他会被包装成一个sudog然后进入到treap里面,启动休眠,当上一个拿到锁的g释放锁后,就会从treap里取出一个sudog唤醒获得锁

3,nwait记录下现在等待该锁的g的数量,原则上来说和treap的数量是一致的

如何加锁?

先看一个重要的方法,如何控制sema信号量

go 复制代码
// 获取信号量
func cansemacquire(addr *uint32) bool {
	for {
		//这里说明一下,sema如果大于0,说明资源充足不需要竞争,
          //如果sema等于0,代表资源紧张,需要互斥竞争同一资源了,协程若没竞争到资源就进入等待状态了
		//这里的sema的数值是在启用的时候就已经初始化设定的
		//也就说,如果我们不设定sema数值,而他的初始值就是0,
          //那么sema锁在这一步永远都是renturn false的,反过来说,他已经退化成一个只有treap的队列,
          //这个很重要,因为在go的底层,很多地方都这么用,
          //他不用sema锁,而是用了semaroot结构体当做一个存储g的队列,
          //比如sync.mutex就是这么用的
		v := atomic.Load(addr)
		if v == 0 {
			// 拿不到,你回去等着吧-->包装成sudog,进入treap进行等候
			return false
		}
		// 交换数值,-1,返回true
		if atomic.Cas(addr, v, v-1) {
			return true
		}
	}
}

这个加锁方法其实就是利用了底层的atomic包的方法,而从汇编来看,atomic的CAS操作其实就是调用了CPU级别的LOCK命令,从这个方面来说,cas的操作是可靠的,直接从cpu层面进行加锁,再多线程竞争也不会出问题

再看加锁的方法:

go 复制代码
func semacquire1(addr *uint32, lifo bool, profile semaProfileFlags, skipframes int, reason waitReason) {
	// 获取当前操作该锁的goroutine
	gp := getg()
	// gp.m.curg  就是指向当前线程(M)上正在执行的 goroutine 的指针。
	// 判断获取的g是否是g所属m当前运行的g,防止你正在操作锁的时候,m已经切换到下一个g了
	if gp != gp.m.curg {
		throw("semacquire not on the G stack")
	}

	// 尝试获取sema信号,获取成功就返回,意思是拿到锁了
	if cansemacquire(addr) {
		return
	}

	// 没拿到锁
	// 初始化创建一个sudog对象
	s := acquireSudog()
	// 获取全局的sematable的根节点,这个地方有点难以理解,go的整个全局最大能同时存在251个semaroot,实际上在快速处理的情况下,go很难同时把251个都塞满,极端情况下塞满的话,就得考虑分布式拆分服务了,单个服务已经庞大到需要同时存在251把锁,这服务的复杂度难以想象
	root := semtable.rootFor(addr)
	t0 := int64(0)
	s.releasetime = 0
	s.acquiretime = 0
	s.ticket = 0
	// 这个是阻塞分析用的,一般来说不用管,除非你搞底层研究,阻塞分析需要记录时间,这里的逻辑都是处理时间的
	if profile&semaBlockProfile != 0 && blockprofilerate > 0 {
		t0 = cputicks()
		s.releasetime = -1
	}
	if profile&semaMutexProfile != 0 && mutexprofilerate > 0 {
		if t0 == 0 {
			t0 = cputicks()
		}
		s.acquiretime = t0
	}
	// 循环处理
	for {
		// 按一定的规则判断锁的顺序,如果不按这个顺序,直接判定为死锁,一般不用管,必须开启GOEXPERIMENT=staticlockranking才有这玩意,这玩意默认关闭
		lockWithRank(&root.lock, lockRankRoot)
		// 等待锁的g的计数器+1
		root.nwait.Add(1)
		// 再次尝试获取锁。成功就退出循环,没啥好说的
		if cansemacquire(addr) {
			root.nwait.Add(-1)
			unlock(&root.lock)
			break
		}
		// 再次尝试也没拿到锁,进入treap的那个队列
		root.queue(addr, s, lifo)
		// 执行gopark,这方法非常重要,但是不需要关注,gopark是go语言底层的一个方法,他的作用是让goroutine挂起等待,换个说法就是休眠,等待被唤醒。它广泛存在于go底层中,但是因为是底层,所以一般来说和应用开发员关系不大,只需要知道他的作用是让g休眠就行
		goparkunlock(&root.lock, reason, traceEvGoBlockSync, 4+skipframes)
		// 从阻塞中被唤醒了,开始获取锁,没有获取成功,继续for循环
		if s.ticket != 0 || cansemacquire(addr) {
			break
		}
	}
	// 依然是阻塞分析不用管
	if s.releasetime > 0 {
		blockevent(s.releasetime-t0, 3+skipframes)
	}
	// 释放sudog,已经拿到锁就释放了
	releaseSudog(s)
}
如何解锁?
go 复制代码
func semrelease1(addr *uint32, handoff bool, skipframes int) {
	// 通过addr在全局的sematable的里找对应的semaroot,和加锁那边一样
	root := semtable.rootFor(addr)
	// 给sema信号+1,意思是释放锁
	atomic.Xadd(addr, 1)

	// 查是否有等待的 Goroutine,即等待在锁上的 Goroutine 数量。如果没有等待的 Goroutine,则返回,不需要唤醒其他 Goroutine。
	if root.nwait.Load() == 0 {
		return
	}

	// 对semaroot里面的lock进行操作上锁,防止冲突
	lockWithRank(&root.lock, lockRankRoot)
	// 再次检查是否有等待的g
	if root.nwait.Load() == 0 {
		//如果没有等待的g
		//解锁semaroot的lock
		unlock(&root.lock)
		return
	}
	// 如果有等待的g
	// 从等待队列里取出一个等待的sudog,让他开始他的逻辑
	s, t0 := root.dequeue(addr)
	if s != nil {
		// 如果treap里面不为空,取出sudog成功,就把等待数量-1
		root.nwait.Add(-1)
	}
	//解锁semaroot的lock
	unlock(&root.lock)
	if s != nil {
		// 检测用的,不用管
		acquiretime := s.acquiretime
		if acquiretime != 0 {
			mutexevent(t0-acquiretime, 3+skipframes)
		}
		if s.ticket != 0 {
			throw("corrupted semaphore ticket")
		}
		if handoff && cansemacquire(addr) {
			s.ticket = 1
		}
		readyWithTime(s, 5+skipframes)
		// 当g的m不持有其他锁的时候才允许调度
		if s.ticket == 1 && getg().m.locks == 0 {
			// 行协程的切换操作,将当前 Goroutine 切换出执行,并且将等待的 Goroutine 放入当前的 P 的本地运行队列,以便被尽快执行。
			// 这里会优先分配给本地队列,在饥饿状态下,切换非常的直接,会直接让切换的g使用当前g没有用完的时间片
			goyield()
		}
	}
}
相关推荐
DemonAvenger7 小时前
深入剖析 sync.Once:实现原理、应用场景与实战经验
分布式·架构·go
一个热爱生活的普通人1 天前
Go语言中 Mutex 的实现原理
后端·go
孔令飞1 天前
关于 LLMOPS 的一些粗浅思考
人工智能·云原生·go
小戴同学1 天前
实时系统降低延时的利器
后端·性能优化·go
Golang菜鸟2 天前
golang中的组合多态
后端·go
Serverless社区2 天前
函数计算支持热门 MCP Server 一键部署
go
Wo3Shi4七2 天前
二叉树数组表示
数据结构·后端·go
网络研究院2 天前
您需要了解的有关 Go、Rust 和 Zig 的信息
开发语言·rust·go·功能·发展·zig
27669582922 天前
拼多多 anti-token unidbg 分析
java·python·go·拼多多·pdd·pxx·anti-token
程序员爱钓鱼3 天前
Go 语言邮件发送完全指南:轻松实现邮件通知功能
后端·go·排序算法