【Go】锁相关

文章目录

  • Mutex锁
    • mutex源码分析
      • Lock
      • UnLock
    • mutex两种运行模式
      • mutex normal 正常模式
        • 自旋
      • mutex starvation 饥饿模式
    • 锁的底层实现类型
  • RWMutex
    • RWMutex 实现
      • 其他共享内存线程安全的方式
  • 思考
    • 如何设计一个并发更高的锁?

Mutex锁

mutex源码分析

Locker接口:

type Locker interface {Lock()Unlock()
}

Mutex 就实现了这个接口,Lock请求锁,Unlock释放锁

type Mutex struct {state int32   //锁状态,保护四部分含义sema  uint32  //信号量,用于阻塞等待或者唤醒
}

在这里插入图片描述

  • Locked:表示该 mutex 是否被锁定,0 表示没有,1 表示处于锁定状态;

  • Woken:表示是否有协程被唤醒,0 表示没有,1 表示有协程处于唤醒状态,并且在加锁过程中;

  • Starving:Go1.9 版本之后引入,表示 mutex 是否处于饥饿状态,0 表示没有,1 表示有协程处于饥饿状态;

  • Waiter: 等待锁的协程数量。

方法解析

const (// mutex is locked ,在低位,值 1mutexLocked = 1 << iota//标识有协程被唤醒,处于 state 中的第二个 bit 位,值 2mutexWoken//标识 mutex 处于饥饿模式,处于 state 中的第三个 bit 位,值 4mutexStarving// 值 3,state 值通过右移三位可以得到 waiter 的数量// 同理,state += 1 << mutexWaiterShift,可以累加 waiter 的数量mutexWaiterShift = iota// 标识协程处于饥饿状态的最长阻塞时间,当前被设置为 1msstarvationThresholdNs = 1e6
)

Lock

func (m *Mutex) Lock() {// Fast path: grab unlocked mutex. //运气好,直接加锁成功if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {if race.Enabled {race.Acquire(unsafe.Pointer(m))}return}// Slow path (outlined so that the fast path can be inlined)//内联,加锁失败,就得去自旋竞争或者饥饿模式下竞争m.lockSlow()
}
func (m *Mutex) lockSlow() {var waitStartTime int64// 标识是否处于饥饿模式starving := false// 唤醒标记awoke := false// 自旋次数iter := 0old := m.statefor {// 非饥饿模式下,开启自旋操作// 从 runtime_canSpin(iter) 的实现中(runtime/proc.sync_runtime_canSpin)可以知道,// 如果 iter 的值大于 4,将返回 falseif old&(mutexLocked|mutexStarving) == mutexLocked && runtime_canSpin(iter) {// 如果没有其他 waiter 被唤醒,那么将当前协程置为唤醒状态,同时 CAS 更新 mutex 的 Woken 位if !awoke && old&mutexWoken == 0 && old>>mutexWaiterShift != 0 &&atomic.CompareAndSwapInt32(&m.state, old, old|mutexWoken) {awoke = true}// 开启自旋runtime_doSpin()iter++// 重新检查 state 的值old = m.statecontinue}new := old// 非饥饿状态if old&mutexStarving == 0 {// 当前协程可以直接加锁new |= mutexLocked}// mutex 已经被锁住或者处于饥饿模式// 那么当前协程不能获取到锁,将会进入等待状态if old&(mutexLocked|mutexStarving) != 0 {// waiter 数量加 1,当前协程处于等待状态new += 1 << mutexWaiterShift}// 当前协程处于饥饿状态并且 mutex 依然被锁住,那么设置 mutex 为饥饿模式if starving && old&mutexLocked != 0 {new |= mutexStarving}if awoke {if new&mutexWoken == 0 {throw("sync: inconsistent mutex state")}// 清除唤醒标记// &^ 与非操作,mutexWoken: 10 -> 01// 此操作之后,new 的 Locked 位值是 1,如果能够成功写入到 m.state 字段,那么当前协程获取锁成功new &^= mutexWoken}// CAS 设置新状态成功if atomic.CompareAndSwapInt32(&m.state, old, new) {// 旧的锁状态已经被释放并且处于非饥饿状态// 这个时候当前协程正常请求到了锁,就可以直接返回了if old&(mutexLocked|mutexStarving) == 0 {break}// 处理当前协程的饥饿状态// 如果之前已经处于等待状态了(已经在队列里面),那么将其加入到队列头部,从而可以被高优唤醒queueLifo := waitStartTime != 0if waitStartTime == 0 {// 阻塞开始时间waitStartTime = runtime_nanotime()}// P 操作,阻塞等待runtime_SemacquireMutex(&m.sema, queueLifo, 1)// 唤醒之后,如果当前协程等待超过 1ms,那么标识当前协程处于饥饿状态starving = starving || runtime_nanotime()-waitStartTime > starvationThresholdNsold = m.state// mutex 已经处于饥饿模式if old&mutexStarving != 0 {// 1. 如果当前协程被唤醒但是 mutex 还是处于锁住状态// 那么 mutex 处于非法状态//// 2. 或者如果此时 waiter 数量是 0,并且 mutex 未被锁住// 代表当前协程没有在 waiters 中,但是却想要获取到锁,那么 mutex 状态非法if old&(mutexLocked|mutexWoken) != 0 || old>>mutexWaiterShift == 0 {throw("sync: inconsistent mutex state")}// delta 代表加锁并且将 waiter 数量减 1 两步操作delta := int32(mutexLocked - 1<<mutexWaiterShift)// 非饥饿状态 或者 当前只剩下一个 waiter 了(就是当前协程本身)if !starving || old>>mutexWaiterShift == 1 {// 那么 mutex 退出饥饿模式delta -= mutexStarving}// 设置新的状态atomic.AddInt32(&m.state, delta)break}awoke = trueiter = 0} else {old = m.state}}if race.Enabled {race.Acquire(unsafe.Pointer(m))}
}

解锁操作会根据 Mutex.state 的状态来判断需不需要去唤醒其他等待中的协程。

func (m *Mutex) unlockSlow(new int32) {// new - state 字段原子减 1 之后的值,如果之前是处于加锁状态,那么此时 new 的末位应该是 0// 此时 new+mutexLocked 正常情况下会将 new 末位变成 1// 那么如果和 mutexLocked 做与运算之后的结果是 0,代表 new 值非法,解锁了一个未加锁的 mutexif (new+mutexLocked)&mutexLocked == 0 {throw("sync: unlock of unlocked mutex")}// 如果不是处于饥饿状态if new&mutexStarving == 0 {old := newfor {// old>>mutexWaiterShift == 0 代表没有等待加锁的协程了,自然不需要执行唤醒操作// old&mutexLocked != 0 代表已经有协程加锁成功,此时没有必要再唤醒一个协程(因为它不可能加锁成功)// old&mutexWoken != 0 代表已经有协程被唤醒并且在加锁过程中,此时不需要再执行唤醒操作了// old&mutexStarving != 0 代表已经进入了饥饿状态,// 以上四种情况,皆不需要执行唤醒操作if old>>mutexWaiterShift == 0 || old&(mutexLocked|mutexWoken|mutexStarving) != 0 {return}// 唤醒一个等待中的协程,将 state woken 位置为 1// old - 1<<mutexWaiterShift waiter 数量减 1new = (old - 1<<mutexWaiterShift) | mutexWokenif atomic.CompareAndSwapInt32(&m.state, old, new) {runtime_Semrelease(&m.sema, false, 1)return}old = m.state}} else {// 饥饿模式// 将 mutex 的拥有权转移给下一个 waiter,并且交出 CPU 时间片,从而能够让下一个 waiter 立刻开始执行runtime_Semrelease(&m.sema, true, 1)}
}

UnLock

// 解锁操作
func (m *Mutex) Unlock() {if race.Enabled {_ = m.staterace.Release(unsafe.Pointer(m))}// mutexLocked 位设置为 0,解锁new := atomic.AddInt32(&m.state, -mutexLocked)// 如果此时 state 值不是 0,代表其他位不是 0(或者出现异常使用导致 mutexLocked 位也不是 0)// 此时需要进一步做一些其他操作,比如唤醒等待中的协程等if new != 0 {m.unlockSlow(new)}
}

mutex两种运行模式

饥饿模式是对公平性和性能的一种平衡,它避免了某些 goroutine 长时间的等待锁。在饥饿模式下,优先对待的是那些一直在等待的 waiter。

mutex normal 正常模式

默认情况下,Mutex的模式为normal。

该模式下,协程如果加锁不成功不会立即转入阻塞排队,而是判断是否满足自旋的条件,如果满足则会启动自旋过程,尝试抢锁。

正常模式 高吞吐量
在这里插入图片描述

自旋

自旋是一种多线程同步机制,当前的进程在进入自旋的过程中会一直保持 CPU 的占用,持续检查某个条件是否为真。
在多核的 CPU 上,自旋可以避免 Goroutine 的切换,使用恰当会对性能带来很大的增益,但是使用的不恰当就会拖慢整个程序,所以 Goroutine 进入自旋的条件非常苛刻:

  • 互斥锁只有在普通模式才能进入自旋;
  • runtime.sync_runtime_canSpin 需要返回 true:
    运行在多 CPU 的机器上
  • 当前 Goroutine 为了获取该锁进入自旋的次数小于四次;
  • 当前机器上至少存在一个正在运行的处理器 P 并且处理的运行队列为空;
    https://draveness.me/golang/docs/part3-runtime/ch06-concurrency/golang-sync-primitives/

mutex starvation 饥饿模式

自旋过程中能抢到锁,一定意味着同一时刻有协程释放了锁,我们知道释放锁时如果发现有阻塞等待的协程,还会释放一个信号量来唤醒一个等待协程,被唤醒的协程得到CPU后开始运行,此时发现锁已被抢占了,自己只好再次阻塞,不过阻塞前会判断自上次阻塞到本次阻塞经过了多长时间,如果超过1ms的话,会将Mutex标记为"饥饿"模式,然后再阻塞。

处于饥饿模式下,不会启动自旋过程,也即一旦有协程释放了锁,那么一定会唤醒协程,被唤醒的协程将会成功获取锁,同时也会把等待计数减1。

在饥饿模式下,Mutex 的拥有者将直接把锁交给队列最前面的 waiter。新来的 goroutine 不会尝试获取锁,即使看起来锁没有被持有,它也不会去抢,也不会 spin(自旋),它会乖乖地加入到等待队列的尾部。

如果拥有 Mutex 的 waiter 发现下面两种情况的其中之一,它就会把这个 Mutex 转换成正常模式:

  • 此 waiter 已经是队列中的最后一个 waiter 了,没有其它的等待锁的 goroutine 了;
  • 此 waiter 的等待时间小于 1 毫秒(ms)。

锁的底层实现类型

锁内存总线,针对内存的读写操作,在总线上控制,限制程序的内存访问

锁缓存行,同一个缓存行的内容读写操作,CPU内部的高速缓存保证一致性

锁,作用在一个对象或者变量上。现代CPU会优先在高速缓存查找,如果存在这个对象、变量的缓存行数据,会使用锁缓存行的方式。否则,才使用锁总线的方式。

RWMutex

RWMutex 实现

type RWMutex struct {w           Mutex  // 复用互斥锁能力//写锁信号量   当阻塞写操作的读操作goroutine释放读锁时,通过该信号量通知阻塞的写操作的goroutine;writerSem   uint32 
// 读锁信号量 当写操作goroutine释放写锁时,通过该信号量通知阻塞的读操作的goroutinereaderSem   uint32 // 当前读操作的数量,包含所有已经获取到读锁或者被写操作阻塞的等待获取读锁的读操作数量readerCount int32  // 获取写锁需要等待读锁释放的数量readerWait  int32 
}

通过记录 readerCount 读锁的数量来进行控制,当有一个写锁的时候,会将读 锁数量设置为负数 1<<30。目的是让新进入的读锁等待之前的写锁释放通知读 锁。同样的当有写锁进行抢占时,也会等待之前的读锁都释放完毕,才会开始 21 进行后续的操作。 而等写锁释放完之后,会将值重新加上 1<<30, 并通知刚才 新进入的读锁(rw.readerSem),两者互相限制。

const rwmutexMaxReaders = 1 << 30
func (rw *RWMutex) Lock() {// First, resolve competition with other writers.// 写锁也就是互斥锁,复用互斥锁的能力来解决与其他写锁的竞争// 如果写锁已经被获取了,其他goroutine在获取写锁时会进入自旋或者休眠rw.w.Lock()// 将readerCount设置为负值,告诉读锁现在有一个正在等待运行的写锁(获取互斥锁成功)r := atomic.AddInt32(&rw.readerCount, -rwmutexMaxReaders) + rwmutexMaxReaders// 获取互斥锁成功并不代表goroutine获取写锁成功,我们默认最大有2^30的读操作数目,减去这个最大数目// 后仍然不为0则表示前面还有读锁,需要等待读锁释放并更新写操作被阻塞时等待的读操作goroutine个数;if r != 0 && atomic.AddInt32(&rw.readerWait, r) != 0 {runtime_SemacquireMutex(&rw.writerSem, false, 0)}
}
func (rw *RWMutex) Unlock() {// Announce to readers there is no active writer.// 将readerCount的恢复为正数,也就是解除对读锁的互斥r := atomic.AddInt32(&rw.readerCount, rwmutexMaxReaders)if r >= rwmutexMaxReaders {race.Enable()throw("sync: Unlock of unlocked RWMutex")}// 如果后面还有读操作的goroutine则需要唤醒他们for i := 0; i < int(r); i++ {runtime_Semrelease(&rw.readerSem, false, 0)}// 释放互斥锁,写操作的goroutine和读操作的goroutine同时竞争rw.w.Unlock()
}

读锁


func (rw *RWMutex) RLock() {// 原子操作readerCount 只要值不是负数就表示获取读锁成功if atomic.AddInt32(&rw.readerCount, 1) < 0 {// 有一个正在等待的写锁,为了避免饥饿后面进来的读锁进行阻塞等待runtime_SemacquireMutex(&rw.readerSem, false, 0)}
}
func (rw *RWMutex) RUnlock() {// 将readerCount的值减1,如果值等于等于0直接退出即可;否则进入rUnlockSlow处理if r := atomic.AddInt32(&rw.readerCount, -1); r < 0 {// Outlined slow-path to allow the fast-path to be inlinedrw.rUnlockSlow(r)}
}

其他共享内存线程安全的方式

官方不太推荐使用锁,更多的是通过channel做数据交换

思考

如何设计一个并发更高的锁?

在Go语言中,使用切片来设计并发更高效的锁是一种常见的做法,通常被称为"分段锁"或"分片锁"。

这种技术可以在一定程度上减小锁的粒度,从而提高并发性能。

package mainimport ("fmt""sync""hash/fnv"
)const numSegments = 16type ConcurrentMap struct {segments []sync.Mutexdata     map[interface{}]interface{}
}func NewConcurrentMap() *ConcurrentMap {segments := make([]sync.Mutex, numSegments)data := make(map[interface{}]interface{})return &ConcurrentMap{segments: segments, data: data}
}func (cm *ConcurrentMap) getSegment(key interface{}) *sync.Mutex {hash := hashFunction(key) % numSegmentsreturn &cm.segments[hash]
}func (cm *ConcurrentMap) Get(key interface{}) interface{} {segment := cm.getSegment(key)segment.Lock()defer segment.Unlock()return cm.data[key]
}func (cm *ConcurrentMap) Set(key, value interface{}) {segment := cm.getSegment(key)segment.Lock()defer segment.Unlock()cm.data[key] = value
}// 假设的哈希函数,仅用于示例目的
func hashFunction(key interface{}) int {h := fnv.New32a()// 将键的字节表示写入哈希函数_, _ = h.Write([]byte(fmt.Sprintf("%v", key)))return int(h.Sum32())
}func main() {concurrentMap := NewConcurrentMap()var wg sync.WaitGroupnumItems := 1000for i := 0; i < numItems; i++ {wg.Add(1)go func(index int) {defer wg.Done()key := fmt.Sprintf("key%d", index)concurrentMap.Set(key, index)}(i)}wg.Wait()// 输出结果for i := 0; i < numItems; i++ {key := fmt.Sprintf("key%d", i)fmt.Printf("%s: %v\n", key, concurrentMap.Get(key))}
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/99545.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

MySQL流程控制

流程控制 顺序结构&#xff1a; 程序从上往下依次执行分支结构&#xff1a; 程序按条件进行选择执行&#xff0c;从两条或多条路径中选择一条执行。循环结构&#xff1a; 程序满足一定条件下&#xff0c;重复执行一组语句 针对于MySQL的流程控制语句主要有3类。注意&#xff…

stack,queue,deque的使用

1.stack是后进先出的&#xff0c;这也影响其对应的接口&#xff0c;所能实现的功能也有限&#xff0c;其中主要的功能如下&#xff1a; void test_stack1() {stack<int> st;st.push(1);st.push(2);st.push(3);st.push(4);st.push(5);st.push(6);while (!st.empty()){c…

香港服务器备案会通过吗?

​  对于企业或个人来说&#xff0c;合规备案是网络运营的基本要求&#xff0c;也是保护自身权益的重要举措。以下内容围绕备案展开话题&#xff0c;希望为您解开疑惑。 香港服务器备案会通过吗? 目前&#xff0c;香港服务器无法备案&#xff0c;这是由于国内管理规定的限制…

netty(一):NIO——处理消息边界

处理消息边界 为什么要处理边界 因为会存在半包和粘包的问题 1.客户端和服务端约定一个固定长度 优点&#xff1a;简单 缺点&#xff1a;可能造成浪费 2.客户端与服务端约定一个固定分割符 *缺点 效率低 3.先发送长度&#xff0c;再发送数据 TLV格式&#xff1a; type…

Linux问题--docker启动mysql时提示3306端口被占用

问题描述&#xff1a; 解决方法&#xff1a; 1.如果需要kill掉mysqld服务可以先通过 lsof -i :3306 2. 查询到占用3306的PID&#xff0c;随后使用 kill -15 PID 来kill掉mysqld服务。 最后结果

我的创作纪念日(C++修仙练气期总结)

分享自己最喜欢的一首歌&#xff1a;空想フォレスト—伊東歌詞太郎 机缘 现在想想自己在CSDN创作的原因&#xff0c;一开始其实就是想着拿着博客当做自己的学习笔记&#xff0c;笔记嘛&#xff0c;随便写写&#xff0c;自己看得懂就ok了的态度凸(艹皿艹 )。也是用来作为自己学习…

Android动画进阶指北

原文链接 Android Animation Advanced Tricks 前面的文章介绍了动画的基本使用方法&#xff0c;本文来聊一聊涉及到动画的高级技巧&#xff0c;以及一些非常优质的学习资源和动画三方库和框架。 页面之间的过渡动画 常规的动画都是针对某一页面上的某个元素做动画&#xff0c…

CentOS系统环境搭建(十四)——CentOS7.9安装elasticsearch-head

centos系统环境搭建专栏&#x1f517;点击跳转 关于node的安装请看上一篇CentOS系统环境搭建&#xff08;十三&#xff09;——CentOS7安装nvm&#xff0c;&#x1f517;点击跳转。 CentOS7.9安装elasticsearch-head 文章目录 CentOS7.9安装elasticsearch-head1.下载2.解压3.修…

Vue3.X 掌握基础知识(二)

接着上一篇 Vue3.X 创建简单项目&#xff08;一&#xff09;&#xff0c;我们接着往下了解Vue的一些简单的功能。 一、HomeView.vue的改写复习 这里我们介绍数据的绑定、判断、for循环等功能。 <template><h1>hello world</h1><p v-text"data.na…

计算机网络基础

前言 在你立足处深挖下去,就会有泉水涌出!别管蒙昧者们叫嚷:“下边永远是地狱!” 博客主页&#xff1a;KC老衲爱尼姑的博客主页 博主的github&#xff0c;平常所写代码皆在于此 共勉&#xff1a;talk is cheap, show me the code 作者是爪哇岛的新手&#xff0c;水平很有限&…

C# 设置、获取程序,产品版本号

右键&#xff0c;程序属性。打开“程序集信息” 选择需要设置的版本信息。下面的代码&#xff0c;获取不同的设置内容。 string 其他 Assembly.GetExecutingAssembly().FullName; string 程序集版本 Assembly.GetExecutingAssembly().G…

Tip-Adapter: Training-free Adaption of CLIP for Few-shot Classification

Tip-Adapter: Training-free Adaption of CLIP for Few-shot Classification (Paper reading) Renrui Zhang&#xff0c;Shanghai AI Laboratory&#xff0c;ECCV2022&#xff0c;Cited:45&#xff0c;Code&#xff0c;Paper 1. 前言 对比式视觉-语言预训练&#xff0c;也称…

javaScript:对函数的认识与应用

目录 一.前言 二.函数介绍 A.函数的分类 1.自定义函数 示例 2.匿名函数 声明匿名函数 计时器也是匿名函数 3.立即执行函数 解释 示例 B.函数的返回值 没有参数&#xff0c;没有返回值的函数 示例 没有参数&#xff0c;有返回值的函数 示例 有参数&#xff0c;有…

【Leetcode】108. 将有序数组转换为二叉搜索树

一、题目 1、题目描述 给你一个整数数组 nums ,其中元素已经按 升序 排列,请你将其转换为一棵 高度平衡 二叉搜索树。 高度平衡 二叉树是一棵满足「每个节点的左右两个子树的高度差的绝对值不超过 1 」的二叉树。 示例1: 输入:nums = [-10,-3,0,5,9] 输出:[0,-3,9,-1…

java学习——二叉树

二叉树的种类&#xff1a; 满二叉树&#xff1a;如果一棵二叉树只有度为0的结点和度为2的结点&#xff0c;并且度为0的结点在同一层上&#xff0c;则这棵二叉树为满二叉树。 完全二叉树&#xff1a;在完全二叉树中&#xff0c;除了最底层节点可能没填满外&#xff0c;其余每层…

spring源码分析bean的生命周期(下)

doGetBean()执行过程 createBean()执行过程 一、DependsOn注解 spring创建对象之前会判断类上是否加了DependsOn注解&#xff0c;加了会遍历然后会添加到一个map中&#xff0c;spring会先创建DependsOn注解指定的类 二、spring类加载器 在合并BeanDefinition&#xff0c;确定…

时序数据库influxdb笔记

官方资料 https://docs.influxdata.com/influxdb/v2.7/install/?tLinux https://www.influxdata.com/influxdb/ 安装 1、linux平台下 1&#xff09;下载 2&#xff09;解压 3&#xff09;添加账户&#xff08; adduser influx&#xff09; 4&#xff09;设置目录权限 5…

操作符详解(1)

1. 操作符分类&#xff1a; 算术操作符 移位操作符 位操作符 赋值操作符 单目操作符 关系操作符 逻辑操作符 条件操作符 逗号表达式 下标引用、函数调用和结构成员 2. 算术操作符 - * / % 1. 除了 % 操作符之外&#xff0c;其他的几个操作符可以作用于整数和浮点数。 2. 对…

技术文档如何在线搭建网页形式,方便编辑与管理分享其他人员?

搭建在线技术文档网页形式的平台可以方便编辑、管理和分享给其他人员&#xff0c;促进团队的协作和知识共享。 搭建在线技术文档网页形式的步骤和具体操作的详细介绍&#xff1a; 1. 选择适合的平台 首先&#xff0c;需要选择适合搭建在线技术文档网页形式的平台。市面上有很…

深入完整的带你了解java对象的比较

目录 元素的比较 1.基本类型的比较 2.对象比较的问题 1.运行结果 2.疑问 3.原因 对象的比较 1.覆写基类的equals 2.基于Comparble接口类的比较 3.基于比较器比较 4.三种方式对比 元素的比较 1.基本类型的比较 在Java 中&#xff0c;基本类型的对象可以直接比较大…