golang并发安全-sync.map

news2025/1/18 8:55:28

sync.map解决的问题

golang 原生map是存在并发读写的问题,在并发读写时候会抛出异常

func main() {
	mT := make(map[int]int)
	g1 := []int{1, 2, 3, 4, 5, 6}
	g2 := []int{4, 5, 6, 7, 8, 9}
	go func() {
		for i := range g1 {
			mT[i] = i
		}

	}()
	go func() {
		for i := range g2 {
			mT[i] = i
		}
	}()
	time.Sleep(3 * time.Second)
}

抛出异常 fatal error: concurrent map writes

如果将map换成sync.map 那么就不会出现这个问题,下面就简单说说syn.map怎么实现的

基本结构

Map结构体

// Map类型针对两种常见的用例进行了优化:1-当给定键的条目只写一次但读多次时,如在只增长的缓存中,2-当多个goroutine读取、写入和覆盖不相交的键集的条目时。在这两种情况下,与单独的Mutex或RWMutex配对的Go映射相比,使用Map可以显著减少锁争用。
type Map struct { 
	// 互斥锁mu,操作dirty需先获取mu 
    mu Mutex 
	// read是只读的数据结构,可安全并发访问部分,访问它无须加锁,sync.map的所有操作都优先读read 
    // read中存储结构体readOnly,readOnly中存着真实数据,储存数据时候需要加锁
    // read中可能会存在脏数据:即entry被标记为已删除
    read atomic.Value // readOnly
 	// dirty是可以同时读写的数据结构,访问它要加锁,新添加的key都会先放到dirty中 
    // dirty == nil的情况:
    // 1.被初始化 
    // 2.提升为read后,但它不能一直为nil,否则read和dirty会数据不一致。 
    // 当有新key来时,会用read中的数据(不是read中的全部数据,而是未被标记为已删除的数据,)填充dirty 
    // dirty != nil时它存着sync.map的全部数据(包括read中未被标记为已删除的数据和新来的数据)
    dirty map[interface{}]*entry 
 	// 统计访问read没有未命中然后穿透访问dirty的次数 
    // 若miss等于dirty的长度,dirty会提升成read,提升后可以增加read的命中率,减少加锁访问dirty的次数    
    misses int
}

 readOnly结构体

//第一点的结构read存的就是readOnly
type readOnly struct {
	m       map[any]*entry //m是一个map,key是interface,value是指针entry,其指向真实数据的地址,
	amended bool  // amended等于true代表dirty中有readOnly.m中不存在的entry。
}

entry结构体

type entry struct { 
    // p:
    //     expunged: 删除; nil: 逻辑删除但存在dirty; 数据  
    p unsafe.Pointer // *interface{}
}

Load方法

代码解说

Load:读取数据

// Load 返回 map 中key 对应的值,如果没有值,则返回 nil。
// ok 结果表示是否在 map 中找到了 value。
func (m *Map) Load(key any) (value any, ok bool) {
	read, _ := m.read.Load().(readOnly) // 从read 读取数据,并转换readonly
	e, ok := read.m[key]
	if !ok && read.amended { // readonly没有找到对应数据
		m.mu.Lock()
        // 双重检测:
        // 再检查一次readonly,以防中间有Map.dirty被替换为readonly
		read, _ = m.read.Load().(readOnly)
		e, ok = read.m[key]
		if !ok && read.amended { // 去 dirty查找对应数据
			e, ok = m.dirty[key]
			// 无论Map.dirty中是否有这个key,miss都加一,
            // 若miss大小等于dirty的长度,dirty中的元素会被加到Map.read中 
			m.missLocked()
		}
		m.mu.Unlock()
	}
	if !ok {
		return nil, false
	}
	return e.load()// 若entry.p被删除(等于nil或expunged)返回nil和不存在(false),否则返回对应的值和存在(true)    
}

missLocked:dirty是如何提升为read

func (m *Map) missLocked() {
	m.misses++ // 每次misses+1
	if m.misses < len(m.dirty) {
		return
	}
    // 当misses等于dirty的长度,m.dirty转换readOnly,amended被默认赋值成false  
	m.read.Store(readOnly{m: m.dirty})
	m.dirty = nil
	m.misses = 0
}

流程图

 load: 会先从readOnly查找数据, 如果没有开启加锁,再次访问readOnly, 再次没有再去dirty去查。

Store方法

代码解说

store: 赋值

// Store 设置key value
func (m *Map) Store(key, value any) {
	read, _ := m.read.Load().(readOnly) // 转换readOnly
    // 若key在readOnly.m中且 e.tryStore 不为 false(没有逻辑删除)
	if e, ok := read.m[key]; ok && e.tryStore(&value) {
		return
	}

	m.mu.Lock()
    // 双重检测:
    // 再检查一次readonly,以防中间有Map.dirty被替换为readonly
	read, _ = m.read.Load().(readOnly)
	if e, ok := read.m[key]; ok {
        // entry.p状态是expunged置为nil
        // 如果是逻辑删除就需要清除标记了
		if e.unexpungeLocked() {
			// 之前dirty中没有此key,所以往dirty中添加此key              
			m.dirty[key] = e
		}
        // cas: 赋值
		e.storeLocked(&value)
	} else if e, ok := m.dirty[key]; ok {
		e.storeLocked(&value)
	} else {
        // dirty中没有新数据,往dirty中添加第一个新key        
		if !read.amended {
            // 把readOnly中未标记为删除的数据拷贝到dirty中            
			m.dirtyLocked()
            // amended:true,现在dirty有readOnly中没有的key            
			m.read.Store(readOnly{m: read.m, amended: true})
		}
		m.dirty[key] = newEntry(value)
	}
	m.mu.Unlock()
}

tryStore:尝试写入数据

func (e *entry) tryStore(i *any) bool {
    for {   
        p := atomic.LoadPointer(&e.p)    
        if p == expunged {   // 如果逻辑删除就返回false    
            return false   
        }    

        // 不是就将value写入
        if atomic.CompareAndSwapPointer(&e.p, p, unsafe.Pointer(i)) {      
            return true    
        }  
    }
}

dirtyLocked: 将readOnly 未删除的放到dirty

func (m *Map) dirtyLocked() { 
    if m.dirty != nil {  
        return 
    }       
    // dirty为nil时,把readOnly中没被标记成删除的entry添加到dirty 
    read, _ := m.read.Load().(readOnly)  
    m.dirty = make(map[interface{}]*entry, len(read.m)) 
    for k, e := range read.m {               
        // tryExpungeLocked函数在entry未被删除时返回false,反之返回true    
        if !e.tryExpungeLocked() {    // entry没被删除    
            m.dirty[k] = e 
            
        }  
    }
}

流程图

sync.map不适合用于频繁插入新key-value的场景,因为此操作会频繁加锁访问dirty会导致性能下降。更新操作在key存在于readOnly中且值没有被标记为删除(expunged)的场景下会用无锁操作CAS进行性能优化,否则也会加锁访问dirty。

Delete方法

代码解说

LoadAndDelete:查找删除

func (m *Map) LoadAndDelete(key any) (value any, loaded bool) {
    read, _ := m.read.Load().(readOnly) 
    e, ok := read.m[key]
    if !ok && read.amended { // readOnly不存在此key,但dirty中可能存在               
        // 加锁访问dirty   
        m.mu.Lock()                
        // 双重检测 
        read, _ = m.read.Load().(readOnly)    
        e, ok = read.m[key]                
        // readOnly不存在此key,但是dirty中可能存在    
        if !ok && read.amended {      
            e, ok = m.dirty[key]      
            delete(m.dirty, key)      
            m.missLocked()   // 判断dirty是否可以转换readOnly,可以就转换
        }   
        m.mu.Unlock()  
    }  
    if ok {                
        // 如果entry.p不为nil或者expunged,则把逻辑删除(标记为nil)    
        return e.delete()  
    } 
    return nil, false
}

delete:逻辑删除

func (e *entry) delete() (value any, ok bool) {
	for {
		p := atomic.LoadPointer(&e.p)
		if p == nil || p == expunged { // 已经处理或者不存在
			return nil, false
		}
		if atomic.CompareAndSwapPointer(&e.p, p, nil) { // 逻辑删除
			return *(*any)(p), true
		}
	}
}

流程图

Range方法

代码解说

Range:轮训元素

func (m *Map) Range(f func(key, value any) bool) {
    read, _ := m.read.Load().(readOnly)     
    if read.amended { // 如果dirty存在数据
        m.mu.Lock()         
        // 双重检测      
        read, _ = m.read.Load().(readOnly)         
        if read.amended {              
            // readOnly.amended被默认赋值成false             
            read = readOnly{m: m.dirty}              
            m.read.Store(read)              
            m.dirty = nil              
            m.misses = 0        
        }        
        m.mu.Unlock()    
    }     
    // 遍历readOnly.m   
    for k, e := range read.m {          
        v, ok := e.load()         
        if !ok {             
            continue          
        }          
        if !f(k, v) { 
            break         
        }     
    }
}

流程图 

Range:全部key都存在于readOnly中时,是无锁遍历的,性能最优。如果readOnly只存在Map中的部分key时,会一次性加锁拷贝dirty的元素到readOnly,减少多次加锁访问dirty中的数据。

总结

1- sync.map 结构体加了readOnly 和 dirty 来实现读写分离,load,store, delete,range 每次都会优先访问read,后面访问dirty都会双重检测以防加锁前Map.dirty可能已被提升为read

2- sync.map不适合写多读少,从store 代码中可以看出会频繁加锁访问dirty,双重检测等等,这些都会导致性能下降

3- sync.map 没有提供对read, dirty 的长度方法,这个对象使用在于并发场景下,会额外带来锁竞争的问题

4- misses 是 统计访问read没有未命中然后穿透访问dirty的次数 ,如果等于dirty会转换readOnly

5- entry 有三种类型 expunged: 删除; nil: 逻辑删除但存在dirty; 数据 。其中expunged 会在 unexpungeLocked 方法中进行赋值(在store时候会加锁访问dirty,把readOnly中的未被标记为删除的所有entry指针放到dirty,之前被delete方法标记为删除状态的entry=nil都变为expunged,那这些被标记为expunged的entry将不会出现在dirty中。)

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

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

相关文章

打开相机失败 出现错误的原因

如何解决&#xff1f; Debug中缺少DLL文件 以下参考周姐文档 相机调用步骤 学习相机第三方库的安装 https://blog.csdn.net/Qingshan_z/article/details/117257136书签&#xff1a;QT添加库&#xff08;静态库和动态库&#xff09;_Qingshan_z的博客-CSDN博客_qt添加库 添加文…

在 Android 手机上从SD 卡恢复数据的 6 个有效应用程序

如果您有 Android 设备&#xff0c;您可能会将个人和专业的重要文件保存在设备的 SD 卡上。这些文件包括照片、视频、文档和各种其他类型的文件。您绝对不想丢失这些文件&#xff0c;但当您的 SD 卡损坏时&#xff0c;数据丢失是不可避免的。 幸运的是&#xff0c;您不需要这样…

22、Qt使用QSettings类读/写初始化文件(.ini)和读/写注册表(Registry)

一、读/写初始化文件(.ini) 1、写 使用如下代码会生成"config.ini"文件&#xff0c;文件内容如下&#xff1a; QSettings settings("config.ini", QSettings::IniFormat); settings.setValue("/MySettings/name", "root"); setti…

基于Java Swing的图书管理系统

一、项目总体架构 本项目基于Java Swing框架&#xff0c;数据库采用的是MySQL。项目文件夹如下&#xff1a; 二、项目截图 1.登录和注册界面 2.用户界面 3.管理员管理图书类别 4.管理员管理书籍 5.管理员管理用户 项目总体包括源代码和课程论文&#xff0c;需要源码的…

nc不支持-e时的反弹

如果你想要使用nc反弹shell&#xff0c;但是不能使用-e选项&#xff0c;你可以尝试以下的替代方法&#xff1a; 使用mkfifo或mknod命令创建一个命名管道&#xff0c;然后使用cat命令读取管道中的内容&#xff0c;并将其传递给/bin/sh执行&#xff0c;再将输出重定向到nc连接。…

论文阅读《Rethinking Efficient Lane Detection via Curve Modeling》

目录 Abstract 1. Introduction 2. Related Work 3. BezierLaneNet 3.1. Overview 3.2. Feature Flip Fusion 3.3. End-to-end Fit of a Bezier Curve 4. Experiments 4.1. Datasets 4.2. Evalutaion Metics 4.3. Implementation Details 4.4. Comparisons 4.5. A…

【北亚数据恢复】mysql表被truncate,表数据被delete的数据恢复案例

云服务器数据恢复环境&#xff1a; 华为ECS云服务器&#xff0c;linux操作系统&#xff0c;mysql数据库&#xff08;innodb引擎&#xff09;。作为网站服务器使用。 云服务器故障&#xff1a; 在执行mysql数据库版本更新测试时&#xff0c;误将本应该在测试库上执行的sql脚本执…

搜维尔科技:经脉腧穴虚拟针灸VR虚拟教学平台AcuMap软件案例分享

北京中医药大学经脉腧穴VR虚拟教学平台案例 主要产品 HTCvive &#xff0c;AcuMap&#xff1b; 实施内容 一、项目说明 &#xff08;1&#xff09;穴位取穴与体表解剖标志关系&#xff1b;&#xff08;2&#xff09;穴下层次解剖及周围解剖结构展示&#xff1b; &#xf…

在线客服系统推荐:提升客户满意度与工作效率的利器

客服系统分为售前和售后&#xff0c;售前客户系统是为了能够及时解决客户在购买产品前的问题&#xff0c;通过客服人员让客户了解产品的功能点是能够满足他们的需求点&#xff0c;从未达到转化的目的。 而售后客户系统主要是提供给购买后的客户强大的产品售后支持&#xff0c;…

Android 13 动态启用或禁用IPV6

介绍 客户想要通过APK来控制IPV6的启用和禁用&#xff0c;这里我们通过广播的方式来让客户控制IPV6。 效果展示 adb shell ifconfig 这里我们用debug软件&#xff0c;将下面节点置为1 如图ipv6已被禁用了 echo 1 > /proc/sys/net/ipv6/conf/all/disable_ipv6 修改 接下来…

Eclipse安装Jrebel eclipse免重启加载项目

每次修改JAVA文件都需要重新启动项目&#xff0c;加载时间太长&#xff0c;eclipse安装jrebel控件,避免重启项目节省时间。 1、Help->Eclipse Marketplace 2、搜索jrebel 3、Help->jrebel->Configuration 配置jrebel 4、激活jrebel 5、在红色框中填入 http://jrebel…

Linux 查看系统类型和版本(内核版本 | 发行版本)

Linux 查看系统类型和版本 首先普及下linux系统的版本内容1. 查看linux系统内核版本2. 查看linux系统发行版本 首先普及下linux系统的版本内容 内核版本和发行版本区别 内核版本就是指 Linux 中最基层的代码&#xff0c;版本号如 Linux version 3.10.0-327.22.2.el7.x86_64发行…

新建虚拟环境并与Jupyter内核连接

第一步:在cmd里新建虚拟环境,shap38是新建的虚拟环境的名字 ,python=3.x conda create -n shap38 python=3.8第二步,安装ipykernel,打开anconda powershell prompt: 虚拟环境的文件夹位置,我的如图所示: 进入文件夹并复制地址: 输入复制的文件夹地址更改文件夹:…

Leetcode—62.不同路径【中等】

2023每日刷题&#xff08;七十二&#xff09; Leetcode—62.不同路径 超时dfs代码 class Solution { public:int uniquePaths(int m, int n) {int starti 1, startj 1;int ans 0;function<void(int, int)> dfs [&](int i, int j) {if(i m && j n) {a…

Java中XML的解析

1.采用第三方开元工具dom4j完成 使用步骤 1.导包dom4j的jar包 2.add as lib.... 3.创建核心对象, 读取xml得到Document对象 SAXReader sr new SAXReader(); Document doc sr.read(String path); 4.根据Document获取根元素对象 Element root doc.getRootElement(); …

建筑红板与黑板在实际应用中有何区别?

在建筑行业中&#xff0c;红板和黑板作为常用的建筑模板材料&#xff0c;各自有着独特的应用场景和性能特点。了解这两种板材的区别对于选择合适的建筑材料至关重要。 材质和制作工艺的差异**建筑红板**&#xff1a;通常是指涂有红色脱模油漆的模板&#xff0c;这种油漆能够提高…

windows无命令升级node版本

1. node最新版本下载链接 点击最新下载链接&#xff0c;找到对应版本下载并解压 2. 通过命令where node找到node.exe位置 3. 将该位置的node.exe替换为下载解压的最新node.exe 4. 重新执行node -v查看版本

linux如何清理磁盘,使得数据难以恢复

sda 是硬盘&#xff0c;sda1 和 sda2 是硬盘的两个分区。centos-root 是一个逻辑卷&#xff0c;挂载在根目录 /。 /dev/sda 是硬盘&#xff0c;/dev/sda1 和 /dev/sda2 是硬盘的两个分区。 [rootnode2 ~]# dd if/dev/urandom of/dev/sda bs4M这个命令将从 /dev/urandom 读取随…

C语言停车场模型详解

C语言停车场模型详解 1. 引言2. 代码概述3. 代码详解3.1 定义常量和数据结构3.2 初始化车库3.3 查找车辆所在车库3.4 查找车辆所在的车位3.5 打印车库状态3.6 打印等候车辆3.7 车辆入库3.8 车辆出库3.9 菜单功能3.10 主函数 5.效果展示5.完整代码6. 总结 1. 引言 本文将介绍一…

GC控制器(Garbagecollector)源码解析

KubeController Garbagecollector 本文从源码的角度分析KubeController Garbagecollector相关功能的实现。 本篇kubernetes版本为v1.27.3。 kubernetes项目地址: https://github.com/kubernetes/kubernetes controller命令main入口: cmd/kube-controller-manager/controller-…