首页 > 其他分享 >Golang 的锁

Golang 的锁

时间:2022-09-30 11:02:51浏览次数:48  
标签:信号量 协程 Golang 互斥 临界 进程 等待

前言

go 语言的锁, 一说大家都知道, 一个是互斥锁Mutex, 一个是读写互斥锁RWMutex, 用起来很简单, 但是要想在技术上更进一步, 还是需要了解其原理

基础知识

进程同步

既然是锁, 就意味着在加锁之后, 其他goroutine获取锁, 就需要等待, 这里就需要了解操作系统的进程同步机制

进程同步其实是控制临界区内的权限, 当一个进程进入临界区时, 其他想进入临界区的进程只能等待, 也就是说, 临界区是互斥的, 而临界区内的资源, 则是临界资源

访问临界区有几种情况:

  • 空闲让进: 当没有进程在临界区时, 代表临界区是空闲的, 此时一个进程可以立即进入临界区, 访问临界资源
  • 忙等: 当已经有进程在临界区内时, 代表临界区有人正在访问, 所以其他想要进入临界区的进程必须等待
  • 有限等待: 当进程等待访问临界区时, 设置一定的超时时间, 避免一直等待
  • 让权: 当进程访问临界区发现需要等待时, 立刻释放资源, 避免夯住

信号量

那么在一个进程结束后, 怎么通知其他进程进入呢? 这就需要信号量

信号量分为四种类型:

  • 整形信号量
  • 记录型信号量
  • AND 型信号量
  • 信号量集

以整形信号量为例, 核心是设置一个用于表示资源数量的整形 A, 这个 A 有两种操作,

一个是减小(wait), 一个是增大(signal)

在进入资源区时进行wait操作, 在退出时进行signal操作

通过对信号量的大小对比, 可以获取当前等待进程和正在运行的进程数量与当前的状态

自旋

在进程不能进入临界区时, 会进行等待, 但是并不是立刻就进入等待了, 而是先不停的去侦测这个锁是否有被释放掉, 这个过程被称作自旋, 这也是锁的一种, 在自旋过程中, 如果发现临界区空置了, 就立刻进入临界区.

自旋其实就是隔一段时间访问一次临界区查看是否空闲, 所以自旋过程中, 这个进程并不是等待的

自旋的存在目的是为了更加的高效, 因为有可能在等待时间很短, 自旋可以让进程不必切换状态

而缺点是因为进程不是等待, 还在运作, 会导致资源损耗, 所以在等待时, 先使用自旋进行获取锁, 在几次之后依旧获取不到, 则变为等待状态

自旋的问题

如果每一个新来的进程都在获取不到锁时, 进行回旋, 就会出现一个问题, 比如之前有3个进程在自旋后依旧没有获取到锁, 进入到了等待状态, 此时一个新的进程来了, 他在获取不到锁时, 进入了自旋状态, 而此时占用锁的进程退出了, 那么这一个新的进程就会最先获取到锁, 类似于插队的问题, 这样显然是不公平的, 为了解决这个问题, 一般是为锁增加饥饿状态, 在饥饿状态下, 不允许进程进入自旋, 直接等待

互斥锁

在了解基本的知识后, 我们来套入到 go 语言中

互斥锁在被占用后, 其他协程完全无法访问, 不可读更不可写

// 互斥锁, 占用后不可读也不可写
var lock sync.Mutex
lock.Lock()  // 加锁
lock.Unlock()  // 解锁

结构

sync.Mutex的数据结构如下

type Mutex struct {
	state int32  // 当前互斥锁的状态
	sema  uint32  // 锁的信号量
}

state

state记录了四个状态. 分别是:

  • waiter_num(29bit): 当前等待抢占这个锁的goroutine数量
  • starving(1bit): 当前锁是否处于饥饿状态(0:无/1:有)
  • woken(1bit): 当前锁是否有goroutine已经被唤醒(0:无/1:有)
  • locked(1bit): 当前锁是否被goroutine持有(0:无/1:有)

sema

sema 记录了信号量, 当锁被一个goroutine释放时, 会释放这个信号量, 唤醒之前抢锁的正在阻塞的goroutine来获取锁

运行流程

正常模式

在正常模式下, 等待的协程按照先入先出的方式排列, 当一个协程被信号唤醒后, 这个协程并不是直接获取到锁, 而是和刚刚到达的协程一起竞争锁的所有权.

新到的协程有一个优势, 就是他因为刚到, 现在还在 CPU 上运行, 而唤醒的协程, 刚从等待状态准备启动, 而且新到的协程有可能不止一个, 所以这个被唤醒的协程很大概率抢不到锁.

为了解决这个问题, 被唤醒的这个协程会被放到等待队列的第一位. 由他来第一个获取锁, 如果等待的协程超过1ms 内没有获取到锁, 此时会把这个锁设置为饥饿模式

饥饿模式

在饥饿模式下, 解锁的协程会将锁的所有权直接交给等待队列第一位的协程. 并且新的协程到达也不会进行自旋来获取锁, 而是直接加入等待队列的队尾.

而等待队列中的协程获取到锁的时候, 会查看

  • 自己是否是等待队列的最后一个协程
  • 自己的等待时间是否小于1ms

如果有任意一个满足要求, 则将锁修改为正常模式

锁在初始时, 为正常模式, 正常模式下效率更高, 因为在释放锁的一瞬间新的协程可以迅速获取锁, 而饥饿模式则在有协程等待1ms 之后运行, 可以让等待的协程优先获取到锁

读写锁

读写互斥锁可以添加两种锁, 读锁和写锁, 在读锁上锁时其他协程可读不可写, 写锁上锁时其他协程不可写不可读

// 读写互斥锁
var rwlock sync.RWMutex
rwlock.RLock()  // 读锁, 此时其他协程不可写, 不可读
rwlock.RUnlock()

rwlock.Lock()  // 写锁, 此时其他协程无法写, 可以读
rwlock.Unlock() 

结构

type RWMutex struct {
  	w           Mutex  // 写锁(互斥)
	writerSem   uint32 // 写锁信号量
	readerSem   uint32 // 读锁信号量
	readerCount int32  // 读锁计数器
	readerWait  int32  // 等待读锁释放的协程数
}

读写锁中等级最高的锁还是写锁, 当加上写锁后其他协程无论是读还是写都会阻塞, 所以写锁也是互斥的

运行流程

这里就分为读锁和写锁了, 当用户上写锁, 其实就是上了个互斥锁, 此时所有新的协程都会阻塞, 类似于上面的互斥锁, 这里不过多的赘述

而加读锁, 实际上就是一个数字, readerCount自增, 此时如果加写锁, 会判断读锁计数器是否为0, 为0则上写锁(互斥), 如果加读锁, 就是readerCount自增

如果写锁释放了, 此时有一些协程请求加写锁, 一些请求加读锁, 会优先将锁分配给写锁

如果读锁释放了, 此时有一些协程请求加写锁, 会等待readerCount为0时加写锁

标签:信号量,协程,Golang,互斥,临界,进程,等待
From: https://www.cnblogs.com/chnmig/p/16744166.html

相关文章

  • Golang 常问知识点(简略)
    前言稍微记录一下问题点吧,文章会不断的优化更新有些比较大的知识点,比如锁原理啥的,会单独拎出来写一篇,这里只是大概Go语言的基础数据类型占用大小类型大小......
  • 抽象工厂模式 Golang
    参考链接练习:设计一个电脑主板架构,电脑包括(显卡,内存,CPU)3个固定的插口,显卡具有显示功能(display,功能实现只要打印出意义即可),内存具有存储功能(storage),cpu具有计算功能(calcul......
  • 【Go学习】golang os/exec 执行外部命令
    exec包执行外部命令,它将os.StartProcess进行包装使得它更容易映射到stdin和stdout,并且利用pipe连接i/o.funcLookPath(filestring)(string,error)//LookPath在环境变量......
  • Golang 中的 WaitGroups 指南
    Golang中的WaitGroups指南这个强大功能的简要说明Photoby谢苗鲍里索夫on不飞溅Goroutines是很好用的工具,但是它们有一个问题。在这篇文章中,我们将调查这个问......
  • Golang Redis有序集合(sorted set)
    Redis有序集合(sortedset)和集合一样也是string类型元素的集合,且不允许重复的成员,不同的是每个元素都会关联一个double类型的分数,这个分数主要用于集合元素排序。引用git......
  • golang之Time时间函数
    总结常用time包下的方法1)获取前一天的时间packagemainimport"time"nowTime:=time.Now()yesterdayTime:=nowTime.AddDate(0,0,-1);//年,月,日获取前一......
  • golang 的双向循环链表
                如下为go实现的双向循环列表。packagemainimport("fmt")typeRingstruct{prev,......
  • golang map 和 interface 的一些记录
    golang的map读取是不需要判断key是否存在的,不存在的key会返回默认值。如果map的value是interface,那么interface是需要先进行类型转换的,非要求类型的转换,得到结果是nil。......
  • Golang-常用算法
    快速排序funcQuickSort(sort[]int)[]int{ iflen(sort)<=1{ returnsort } low:=make([]int,0,0) mid:=make([]int,0,0) high:=make([]int,0,0......
  • 【Golang】PHP转Go强大的工具库
    PHP的朋友应该会被PHP中两大特性折服:1、强大的array,一切皆Array;2、强大的函数库,函数类型非常丰富。一、简单示例1、例如:md5函数Go版本写法:packagemainimport......