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()
		}
	}
}
相关推荐
郝同学的测开笔记20 小时前
云原生探索系列(十二):Go 语言接口详解
后端·云原生·go
一点一木2 天前
WebAssembly:Go 如何优化前端性能
前端·go·webassembly
千羽的编程时光2 天前
【CloudWeGo】字节跳动 Golang 微服务框架 Hertz 集成 Gorm-Gen 实战
go
27669582923 天前
阿里1688 阿里滑块 231滑块 x5sec分析
java·python·go·验证码·1688·阿里滑块·231滑块
Moment5 天前
在 NodeJs 中如何通过子进程与 Golang 进行 IPC 通信 🙄🙄🙄
前端·后端·go
唐僧洗头爱飘柔95275 天前
(Go基础)变量与常量?字面量与变量的较量!
开发语言·后端·golang·go·go语言初上手
黑心萝卜三条杠5 天前
【Go语言】深入理解Go语言:并发、内存管理和垃圾回收
google·程序员·go
不喝水的鱼儿5 天前
【LuatOS】基于WebSocket的同步请求框架
网络·websocket·网络协议·go·luatos·lua5.4
微刻时光6 天前
程序员开发速查表
java·开发语言·python·docker·go·php·编程语言
lidenger6 天前
服务认证-来者何人
后端·go