首页 > 其他分享 >go锁基础 - atomic、sema

go锁基础 - atomic、sema

时间:2023-12-01 12:11:20浏览次数:30  
标签:协程 addr sema atomic go root uint32

atomicsema是实现go中锁的基础,简单看下他们的实现原理。

atomic

`atomic 常用来作为保证原子性的操作。

当多个协程,同时一个数据进行操作时候,如果不加锁,最终的很难得到想要的结果。

  var p int64 = 0
  func add() {
  	p = p + 1
  }

  func main() {

  	for i := 0; i < 1000; i++ {
  		go add()
  	}
  	time.Sleep(time.Second * 5)
  	fmt.Println(p) //982
  }

这种情况下,最终打印的 都不会是1000,每次不固定。

改成atomic 能解决
var p int64 = 0
func add() {
	atomic.AddInt64(&p, 1)
}
func main() {

	for i := 0; i < 1000; i++ {
		go add()
	}
	time.Sleep(time.Second * 5)
	fmt.Println(p)
}

atomic 为什么能做到?

 TEXT	sync∕atomic·AddInt64(SB), NOSPLIT, $0-24
	GO_ARGS
	MOVD	$__tsan_go_atomic64_fetch_add(SB), R9
	BL	racecallatomic<>(SB)
	MOVD	add+8(FP), R0	// convert fetch_add to add_fetch
	MOVD	ret+16(FP), R1
	ADD	R0, R1, R0
	MOVD	R0, ret+16(FP)
	RET

老的版本中是能见到,lock 这种操作系统级别的锁,新版的go已经改写了这块逻辑,但是能猜想到效果肯定一样。 如果有理清楚的,评论区可以交流下。

小结:

原子操作是一种硬件层面加锁的机制
保证操作一个变量的时候,其他协程/线程无法访问

sema

几乎在go的每个锁的定义都能看到sema的身影,理解了sema再看 互斥锁、读写锁就会很好理解。

信号量锁/信号锁
核心是一个uint32值,含义是同时可并发的数量
每一个sema 锁都对应一个SemaRoot结构体
SemaRoot中有一个平衡二叉树用于协程排队

例如:

 type Mutex struct {
  	state int32
  	sema  uint32 
 }

sema的uint32中的 每一个数,背后都对应一个  semaRoot的结构体
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.
}

type sudog struct {
        g *g              // 包含了 协程 g
	next *sudog   // 下一个
	prev *sudog  
	elem unsafe.Pointer // data element (may point to stack)
 }

结构如下:

这里可以先讲下,当这个 sema uint32 值,初始化时候,大于0 比如 赋值5,就代表着,并发时候,有5个协程可以获取锁。其他协程需要等待前面5个释放了,才能进入。

sema 大于0

   // 获取sema锁。大于0的情况
func semacquire1(addr *uint32, lifo bool, profile semaProfileFlags, skipframes int, reason waitReason) {
    	gp := getg()
    	if gp != gp.m.curg {
    		throw("semacquire not on the G stack")
    	}
    	// Easy case. // 容易的情况
    	if cansemacquire(addr) {
    		return
    	}
      // 方法很长,先看简单的部分。
}
  
func cansemacquire(addr *uint32) bool {
	for {
		v := atomic.Load(addr) // 根据sema的地址,获取int32的值
		if v == 0 {   // 如果未0了,就获取失败了
			return false
		}
        // 大于0 ,则把 sema的值减去1
		if atomic.Cas(addr, v, v-1) { // cas 就是 CompareAndSwapInt 的底层实现
			return true
		}
	}
}

到此,对sema为什么只是定义为一个 uint32的值有了大致理解,就是一个控制能有多少个协程同时获取锁的值。

看下释放:

func semrelease1(addr *uint32, handoff bool, skipframes int) {
	root := semtable.rootFor(addr)
	atomic.Xadd(addr, 1) // 给sema的值 加上1

	// Easy case: no waiters?
	// This check must happen after the xadd, to avoid a missed wakeup
	// (see loop in semacquire).
	if root.nwait.Load() == 0 { // 如果没有 nwait在等待,就直接结束。 
		return
	}
}

nwait 就是等待协程的个数。

小结, 当sema的值大于0 :

获取锁:uint32减1 ,获取成功

释放锁:uint32加1,释放成功

sema值等于0

再看 semacquire1

func semacquire1(addr *uint32, lifo bool, profile semaProfileFlags, skipframes int, reason waitReason) {

     / / Harder case:
	//	increment waiter count
	s := acquireSudog()
	root := semtable.rootFor(addr) // 根据sema的地址,获取了包含 sudog的队列
	for {
		root.queue(addr, s, lifo) // 将新的协程放入这个等待队列中
		goparkunlock(&root.lock, reason, traceBlockSync, 4+skipframes) 
        // 主动调用协程的gopark,让它休眠,gopark的说明看 go GMP中有讲
	}
	releaseSudog(s)
}

//再看释放
func semrelease1(addr *uint32, handoff bool, skipframes int) {
	root := semtable.rootFor(addr)
	atomic.Xadd(addr, 1)
	if root.nwait.Load() == 0 {
		return
	}
    // 如果等待队列不是0 ,就需要释放一个
	// Harder case: search for a waiter and wake it.
	lockWithRank(&root.lock, lockRankRoot)
	
	s, t0 := root.dequeue(addr) // 从全局的队列中,取出一个
	if s != nil {
		root.nwait.Add(-1) // 把等待的数量减一
	}
	unlock(&root.lock) //操作全局队列都需要加锁
}

小结,当sema的值等于0时候:

获取锁:协程休眠,进入堆树等待
释放锁:从堆树中取出一个协程,唤醒
sema 锁退化成一个专用休眠队列

有没有可能sema的值,小于0 ?

看看sema的定义 `uint32` 所以 不可能。

总结下:

atomic原子操作是一种硬件层面的加锁机制。

sema 背后是一整套锁的管理和等待的机制,开发者在使用时候,感知不到。

sema的值就是能同时获取锁协程的个数。sema的地址作为了休眠等待队列(平衡树)的key。

标签:协程,addr,sema,atomic,go,root,uint32
From: https://www.cnblogs.com/studyios/p/17869424.html

相关文章

  • Golang中如何自定义时间类型进行xml、json的序列化/反序列化
    在日常开发工作中,我们进行会遇到将struct序列化json字符串以及将json字符串反序列化为struct的场景,大家也对此十分熟悉。最近工作中,遇到了需要将struct序列化xml字符串以及将xml字符串反序列化为struct的场景,对于普通类型的字段,比如int、string等类型,直接......
  • Golang学习笔记-定时任务
    指定具体时间执行packagemainimport( "fmt" "time")funcmain(){ //指定执行时间为2023-11-2900:00:00 executionTime:=time.Date(2023,time.November,29,0,0,0,0,time.UTC) //当前时间 now:=time.Now().UTC() //计算距离执行时间的持续时间 d......
  • [good]c语言中各种类型
    #include<stdio.h>#include<stdlib.h>#include<string.h>#include<stdarg.h>#include<assert.h>#include<math.h>#include<time.h>#include<limits.h>#include<float.h>#include<ctype.h>#i......
  • 使用Go Validator在Go应用中有效验证数据
    使用GoValidator在Go应用中有效验证数据原创TimLiu爱发白日梦的后端2023-12-0108:01发表于广东  作为一名开发者,确保Go应用中处理的数据是有效和准确的非常重要。GoValidator是一个开源的数据验证库,为Go结构体提供强大且易于使用的数据验证功能。本篇文章将介绍GoVa......
  • argon主题优化
    字体设置字体可以在字体天下、100font等网站里免费下载。通过转换网站获得woff2格式文件。你可以上传到网站根目录或者某个CDN里,然后添加下列额外CSS(具体方法见下):/*设置网站字体*//*原则上你可以设置多个字体,然后在不同的部位使用不同的字体*/@font-face{font-family:btf......
  • (转)Go实现随机加盐密码认证
    原文:https://juejin.cn/post/7068192471498358821为什么要加密人们往往有使用同一密码的习惯,为了防止数据库意外泄露/破坏和出于保护用户隐私的目的,不应在数据库里存入用户密码明文实现代码 scss复制代码packagemainimport( "fmt" "golang.org/x/crypto/b......
  • Django四件套在响应头中放入数据的方法
    HttpResponse方法一:returnHttpResponse('ok',headers={'xxx':'xxx'})方法二:obj=HttpResponse('ok')obj['yyy']='yyy'#像字典一样放入,最终会放在http的响应头中returnobj redirect无法直接传headers=传入数据方法:obj=......
  • [good]enum
    typedefenum{Reg_Set_Speed=100,//100Reg_Set_Enable_VSP,//101Reg_Set_Dir,//102Reg_Force_Stop}Modbus_Holding_Registors;这是一个C语言中的`enum`(枚举)类型定义。枚举是一种用户定义的数据类型,它可以包含几个用户定义的值。在这个例子中,`M......
  • [good]数据类型
    `uint`是一种无符号整数类型,它的全称是"unsignedint"。这种类型可以表示从0到某个正数的值。具体能表示的最大值取决于实现,但在大多数现代系统上,`uint`通常是32位的,可以表示的最大值是4294967295。与此相比,`uint8_t`和`uint32_t`是固定宽度的整数类型,它们的位宽分别是8位和32位......
  • supervisor管理启动重启,Java,Go程序Demo
    简介Supervisor是一款Python开发的进程管理系统,允许用户监视和控制Linux上的进程,能将一个普通命令行进程变为后台守护进程,异常退出时能自动重启1、安装yum-yinstallsupervisor2、配置默认配置文件echo_supervisord_conf>/etc/supervisord.conf3、修改主配置文件......