【Go】锁相关

news2024/7/4 4:57:20

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 ,在低位,值 1
   mutexLocked = 1 << iota

    //标识有协程被唤醒,处于 state 中的第二个 bit 位,值 2
   mutexWoken

    //标识 mutex 处于饥饿模式,处于 state 中的第三个 bit 位,值 4
   mutexStarving

    // 值 3,state 值通过右移三位可以得到 waiter 的数量
   // 同理,state += 1 << mutexWaiterShift,可以累加 waiter 的数量
   mutexWaiterShift = iota

    // 标识协程处于饥饿状态的最长阻塞时间,当前被设置为 1ms
   starvationThresholdNs = 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 := 0
   old := m.state
   for {
      // 非饥饿模式下,开启自旋操作
      // 从 runtime_canSpin(iter) 的实现中(runtime/proc.sync_runtime_canSpin)可以知道,
      // 如果 iter 的值大于 4,将返回 false
      if 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.state
         continue
      }
      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 != 0
         if waitStartTime == 0 {
            // 阻塞开始时间
            waitStartTime = runtime_nanotime()
         }
         // P 操作,阻塞等待
         runtime_SemacquireMutex(&m.sema, queueLifo, 1)
         // 唤醒之后,如果当前协程等待超过 1ms,那么标识当前协程处于饥饿状态
         starving = starving || runtime_nanotime()-waitStartTime > starvationThresholdNs
         old = 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 = true
         iter = 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 值非法,解锁了一个未加锁的 mutex
   if (new+mutexLocked)&mutexLocked == 0 {
      throw("sync: unlock of unlocked mutex")
   }
   // 如果不是处于饥饿状态
   if new&mutexStarving == 0 {
      old := new
      for {
         // 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 数量减 1
         new = (old - 1<<mutexWaiterShift) | mutexWoken
         if 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.state
      race.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的实现比Mutex要简单。
RWMutex的结构中有两个信号量,分别对应于读者和写者,
另外两个整数记录读者和写者的数量。
RWMutex的加解锁过程都是两步,首先原子操作修改读者或写者计数,然后直接获取信号量,与Mutex相比省去了fastpath和自旋锁过程。由此可见,当多读少写的场景中,使用RWMutex的效率应当会高于Mutex,因为省去了大量的忙等待过程。

更细力度的锁,在读读操作非互斥,
正常业务下读多写少,用mutex互斥的话,并发性能不高,
RWMutex读不会产生互斥,并发性能会好一些

项目中读写锁会多一些,有一些加载到内存的一些共享数据

自旋的协程时间过长会空耗cpu资源, cpu nginx_cpu_pause函数会调用汇编中的pause指令, 防止cpu过度消费cpu资源

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

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

思考

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

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

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

package main

import (
	"fmt"
	"sync"
	"hash/fnv"
)

const numSegments = 16

type ConcurrentMap struct {
	segments []sync.Mutex
	data     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) % numSegments
	return &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.WaitGroup
	numItems := 1000

	for 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.coloradmin.cn/o/888986.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

IDEA启动报错【java.sql.SQLSyntaxErrorException: ORA-00904: “P“.“PRJ_NO“: 标识符无效】

IDEA报错如下&#xff1a; 2023-08-17 11:26:15.535 ERROR [egrant-biz,b48324d82fe23753,b48324d82fe23753,true] 24108 --- [ XNIO-1 task-1] c.i.c.l.c.RestExceptionController : 服务器异常org.springframework.jdbc.BadSqlGrammarException: ### Error queryin…

看看安森美深力科NSI45090JDT4G 是如何点亮汽车内外照明系统解决方案

关于线性恒流调节器&#xff08;CCR&#xff09;&#xff1a;是一种用于控制电流的稳定输出。它通常由一个功率晶体管和一个参考电流源组成。CCR的工作原理是通过不断调节功率晶体管的导通时间来维持输出电流的恒定。当输出电流超过设定值时&#xff0c;CCR会减少功率晶体管的导…

C# WPF ListBox 动态显示图片

前言 最近在和其他软件联合做一个本地图片选择传输功能&#xff0c;为此希望图片能够有序的呈现在客户端&#xff0c;简单的实现了一下功能&#xff0c;通过Mvvm模式进行呈现&#xff0c;过程简单通俗&#xff0c;话不多说直接上图。 处理过程 前台代码 你只需要粘贴到你的前台…

linux动态链接之.plt与.got.plt

1. 动态链接 一个 main.c 文件 #include <stdio.h> #include <stdlib.h>int main() {puts("123");puts("456");return 0; }此时我们编译它默认会使用动态链接默认生成a.out可执行文件 gcc main.c当第一次调用puts函数时&#xff0c;会去往.…

考公-判断推理-逻辑判断-真假推理

真假推理最多考一道 例题 找矛盾 例题 例题 四组 例题 例题 例题 例题 例题 例题 例题

Keburnetes 配置资源管理 Secret ConfigMap

Secret 资源 用于保存密码文件、tls证书/私钥文件、token令牌字符串、镜像私有仓库认证信息 等需要加密的敏感的信息 Secret资源的 4 种类型 Opaque&#xff1a;默认的Secret资源类型&#xff0c;可以通过选项引用文件、目录、键值对的方式创建Secret资源&#xff0c;并且资…

在APP中如何嵌入小游戏?

APP内嵌游戏之所以能火爆&#xff0c;主要是因为互联网对流量的追求是无止境的&#xff0c;之前高速增长的红利期后&#xff0c;获取新的流量成为各大厂商的挑战&#xff0c;小游戏的引入&#xff0c;就是这个目的&#xff0c;为已有的产品赋能&#xff0c;抢占用户注意力和使用…

文件夹数据加密怎么设置?文件夹数据加密软件哪个好?

文件夹是电脑存放重要数据的载体&#xff0c;我们需要保护其数据安全&#xff0c;那么文件夹数据加密该怎么设置呢&#xff1f;下面我们就来盘点一下文件夹数据加密软件。 EFS加密 提起文件夹加密&#xff0c;很多人想到的就是EFS加密&#xff0c;它可以加密NTFS卷的各种文件或…

【RP2040】香瓜树莓派RP2040之新建工程

本文最后修改时间&#xff1a;2022年09月05日 11:02 一、本节简介 本节介绍如何新建一个自己的工程。 二、实验平台 1、硬件平台 1&#xff09;树莓派pico开发板 ①树莓派pico开发板*2 ②micro usb数据线*2 2&#xff09;电脑 2、软件平台 1&#xff09;VS CODE 三、版…

ThreadLocal内存泄漏问题

引子&#xff1a; 内存泄漏&#xff1a;是指本应该被GC回收的无用对象没有被回收&#xff0c;导致内存空间的浪费&#xff0c;当内存泄露严重时会导致内存溢出。Java内存泄露的根本原因是&#xff1a;长生命周期的对象持有短生命周期对象的引用&#xff0c;尽管短生命周期对象已…

为什么 ThreadLocal 会导致内存泄漏 ?如何解决 ?

面试官问 &#xff1a;为什么 ThreadLocal 会导致内存泄漏 &#xff1f;&#xff1f; 在面试中被问到这个问题&#xff0c;如果记不清细节了&#xff0c;可以这样回答&#xff1a; ThreadLocal 里面存储的数据&#xff0c;它的生命周期是和线程或者线程池的生命周期保持一致的…

JavaWeb-DAO设计模式

目录 DAO设计模式 1.认识DAO 2.DAO各部分的详解 3.DAO设计模式流程 DAO设计模式 1.认识DAO DAO(Data Acess Object 数据库访问对象)的主要功能是操作数据库&#xff0c;所以DAO在标准开发架构中数据数据层&#xff0c;以下是标准开发的架构 客户层&#xff1a;目前使用B/…

ZKSV2-12-2NCP、ZKSV2-08-2NCSP-L、ZKSV2-08-2NOP插装式电磁换向阀

ZKSV-08-2NCP、ZKSV2-08-2NCP、ZKSV2-10-2NCP、ZKSV2-12-2NCP、ZKSV2-08-2NCSP-L、ZKSV2-08-2NOP、ZKSV2-10-2NOP、ZKSV2-12-2NOP、ZKSV2-08-3、ZKSV2-08-3A、ZKSV2-08-3C、ZKSV2-08-3B、ZKSV2-10-3、ZKSV2-10-3C、ZKSV2-10-3B、ZKSV2-08-4 CO、ZKSV2-08-4 NC、ZKSV2-08-4 TC、…

问道管理:机器人概念走势活跃,新时达涨停,拓斯达、丰立智能等大涨

机器人概念17日盘中走势活跃&#xff0c;到发稿&#xff0c;拓斯达大涨18%&#xff0c;昊志机电涨近16%&#xff0c;丰立智能涨超13%&#xff0c;步科股份、优德精细涨超10%&#xff0c;新时达涨停&#xff0c;天玑科技、兆龙互联、中大力德涨逾9%。 消息面上&#xff0c;8月16…

javaScript:一分钟看懂数组排序以及冒泡排序(重点)

目录 一.前言 二.数组排序 sort&#xff08;&#xff09; 字符串大小的比较原则 字符编码排序规则 如果使用sort从小到大排列 &#xff08;重点&#xff09; 函数的返回值 规则&#xff1a; 代码 案例 数组打乱 sort相关代码 三.冒泡排序&#xff08;重点&#xff…

Zass主题 - 手工艺术家和工匠的WooCommerce商城主题

Zass主题是适合手工艺术家和工匠的完美 WordPress / WooCommerce 主题。无论您是想为您的手工制作业务构建功能齐全的 Etsy Style 在线商店、博客还是作品集&#xff0c;Zass 主题都是您的正确选择。凭借其极其强大的自定义电子商务功能、无限的作品集风格、不同的博客风格和无…

多维时序 | MATLAB实现WOA-CNN-GRU-Attention多变量时间序列预测

多维时序 | MATLAB实现WOA-CNN-GRU-Attention多变量时间序列预测 目录 多维时序 | MATLAB实现WOA-CNN-GRU-Attention多变量时间序列预测预测效果基本介绍模型描述程序设计参考资料 预测效果 基本介绍 MATLAB实现WOA-CNN-GRU-Attention多变量时间序列预测&#xff0c;WOA-CNN-GR…

小象课堂在线授课教育系统

此项目包含后端全部代码&#xff0c;前端包括后台和web界面的源码&#xff0c;数据库用的mysql,可当作课设或者毕设&#xff0c;还可写入自己的简历中 web界面展示&#xff1a; 前端后台界面展示&#xff1a; 用户管理 课程管理 内容配置 订单管理 系统管理 系统监控

Postman项目实战一

项目简介&#xff1a;外卖任务委派系统 测试脚本&#xff1a; 登录&#xff0c;获取token创建任务&#xff0c;获取任务id根据id&#xff0c;修改任务根据id&#xff0c;查询任务根据id&#xff0c;删除任务根据id&#xff0c;验证任务已被删除 步骤&#xff1a; 1.创建Col…

聚隆转债上市价格预测

聚隆转债 基本信息 转债名称&#xff1a;聚隆转债&#xff0c;评级&#xff1a;A&#xff0c;发行规模&#xff1a;2.185亿元。 正股名称&#xff1a;南京聚隆&#xff0c;今日收盘价&#xff1a;16.64元&#xff0c;转股价格&#xff1a;18.27元。 当前转股价值 转债面值 / 转…