Go-知识sync map

news2025/1/16 1:58:55

Go-知识sync map

  • 1. 用法
    • 1.1 声明
    • 1.2 增删改查
    • 1.3 增强操作
  • 2. sync map 使用注意
    • 2.1 多读少写
    • 2.2 类型安全风险
    • 2.3 不能拷贝和传递
  • 3. 实现原理
    • 3.1 数据结构
    • 3.2 read表数据结构
    • 3.3 entry 的数据结构
    • 3.4 sync map 的结构图
    • 3.5 插入数据
    • 3.6 查找数据
    • 3.7 再次插入
    • 3.8 删除数据
  • 4. 总结

一个小活动: https://developer.aliyun.com//topic/lingma/activities/202403?taskCode=14508&recordId=40dcecb786f9a65c2e83e95306822ce4#/?utm_content=m_fission_1 「通义灵码 · 体验 AI 编码,开 AI 盲盒」

githubio地址:https://a18792721831.github.io/

map 是一个并发不安全的key-value存储映射工具,在Go支持高性能并发的语言中,如果多个 goroutine 之间传递map,
在并发的过程中非常容易触发读写冲突,导致程序panic。
sync map 是并发安全的key-value映射。

1. 用法

1.1 声明

sync map不需要想map那样,使用make或者使用剪短变量声明赋值初始,可以直接使用,零值为空 sync map ,不是nil.

    var sm sync.Map

1.2 增删改查

增删改查比较简单:

func TestSyncMap(t *testing.T) {
	var sm sync.Map
	// 增加或修改
	sm.Store("hi", "hello")
	// 查询
	// 查询返回 value, bool,必须显示忽略 bool
	v, ok := sm.Load("hi")
	fmt.Printf("time : %s, v = %s, ok = %v", time.Now().Format(T_F), v, ok)
	// 删除
	sm.Delete("hi")
	wd := sync.WaitGroup{}
	wd.Add(2)
	fmt.Printf("time : %s , start\n", time.Now().Format(T_F))
	go func() {
		rand.Seed(time.Now().UnixNano())
		fmt.Printf("time : %s , sleep\n", time.Now().Format(T_F))
		time.Sleep(time.Duration(rand.Intn(10)) * time.Second)
		// 查询并增加,返回 oldV, bool , 如果 key 已经有值,返回 v 和 true ,否则返回 nil 和 false
		oldValue, isExists := sm.LoadOrStore("hi", "world")
		fmt.Printf("time : %s ,old := %s , is Exists : %v\n", time.Now().Format(T_F), oldValue, isExists)
		wd.Done()
	}()
	go func() {
		rand.Seed(time.Now().UnixNano())
		fmt.Printf("time : %s , sleep\n", time.Now().Format(T_F))
		time.Sleep(time.Duration(rand.Intn(5)) * time.Second)
		oldValue, isExists := sm.LoadOrStore("hi", "worldX")
		fmt.Printf("time : %s ,old := %s , is Exists : %v\n", time.Now().Format(T_F), oldValue, isExists)
		fmt.Printf("time : %s ,delete map\n", time.Now().Format(T_F))
		sm.Delete("hi")
		wd.Done()
	}()
	wd.Wait()
}

在这里插入图片描述

与map不同的是,sync map 不能使用[]来指定key,因为map是标准库提供的,编译的时候会做链接。
还需要注意一点,sync map 能存储任何类型的key-value,key不在限制为基本类型。
但是这也意味着,如果无法保证value的类型,那么在使用的时候,需要使用类型断言。

1.3 增强操作

除了map中的简单的增删改查之外,还有一些结合了查询的操作。

  • LoadOrStore
// LoadOrStore returns the existing value for the key if present.
// LoadOrStore 返回旧值,如果不存在,返回给定的值
// Otherwise, it stores and returns the given value.
// The loaded result is true if the value was loaded, false if stored.
// 如果有旧值,返回true,否则返回false
func (m *Map) LoadOrStore(key, value interface{}) (actual interface{}, loaded bool) {}
  • LoadAndDelete
// LoadAndDelete deletes the value for a key, returning the previous value if any.
// LoadAndDelete 删除给定的key,如果存在,返回值,如果不存在返回 nil
// The loaded result reports whether the key was present.
// 如果给定的key对应的只存在,返回true,否则返回false
func (m *Map) LoadAndDelete(key interface{}) (value interface{}, loaded bool) {}
  • Range
func (m *Map) Range(f func(key, value interface{}) bool) {} 

sync map无法像map一样使用range进行遍历,所以提供了Range方法实现遍历能力。 Range会遍历每一个key-value,并对每一个key-value调用传入的函数,实现遍历。

因为sync map支持并发读写,遍历期间可能读取到其他goroutine写入的数据。也就是说,遍历过程中,map是动态变化的。

2. sync map 使用注意

sync map是用于解决并发情况下map的读写冲突问题的,sync map不是为了替代map,仅仅是map的一个优化实现。
sync map提供并发读写的能力也是有代价的,引入了一些限制或者是风险。

2.1 多读少写

sync map内部实现采用了两个原生map实现读写分离,数据读取并且能命中才能提升读取的性能,否则因为要遍历两个map,性能不如原生map。
由于sync map 使用了两个冗余的原生map,就会使用更多的内存存储数据,会对系统的内存有相对高的要求。
当无法确定是否使用sync map的时候,可以采用benchmark进行性能测试。

2.2 类型安全风险

在sync map中,不管是key还是value都是使用interface{}类型存储的,不是像map一样,指定了key和value的类型。
所以在使用的时候,需要做类型断言。(Go支持了泛型之后会有改善)
比如:

func TestSyncMapType(t *testing.T) {
	sm := sync.Map{}
	sm.Store("hi", true)
	sm.Store(10, "haha")
	sm.Range(func(key, value interface{}) bool {
		fmt.Printf("key : %v , type : %s , value : %v , type : %s\n", key, reflect.TypeOf(key).String(), value, reflect.TypeOf(value).String())
		return true
	})
}

在这里插入图片描述

虽然在存储的时候,任何类型都能成功的存储,但是也带来了读取的类型困扰,这意味这读取到的key-value的类型无法确定,必须先使用类型断言后才能使用。

2.3 不能拷贝和传递

因为sync map中使用sync.Mutex实现并发安全,锁是不能拷贝的,否则会导致死锁或者panic,所以在使用sync map的时候,优先使用指针,这样函数参数拷贝时,拷贝的是指针,而不是sync map本身。

Go编译器无法识别这个风险,特别注意。

3. 实现原理

3.1 数据结构

sync/map.go中定义了sync map:

type Map struct {
    // 锁
	mu Mutex
	// 读表,允许并发读
	read atomic.Value // readOnly
	// 写临时表
	dirty map[interface{}]*entry
	// 查找读表丢失次数
	misses int
}

sync map 由两个map表组成,read表提供并发读的能力,新数据则写入dirty表。read表的数据类型虽然是原子类型,但是存放的是map。
dirty表是新数据的临时存放区,数据最终会同步到read表,同步的时机则取决于misses。读取数据时先查询read表,如果未找到,则misses++,在查询dirty表。
等misses达到一定的数量时,触发数据同步。
锁主要是保护dirty表,同时在数据同步时,也起到保护作用。

3.2 read表数据结构

在sync map中,read表是atomic.Value类型,实际的类型是readOnly结构体:

// readOnly is an immutable struct stored atomically in the Map.read field.
type readOnly struct {
	m       map[interface{}]*entry
	amended bool // true if the dirty map contains some key not in m.
	// 标记dirty表中是否存在数据未同步
}

amended用于指示在查询数据时,是否需要继续查询dirty表,如果amended=false,那么当read表中没有找到key,那么就不在查询dirty表,而是直接返回nil.
这样就节省了一次加锁,遍历dirty表的时间。

3.3 entry 的数据结构

不管是dirty表还是read表,类型都是entry指针,entry的定义:

// An entry is a slot in the map corresponding to a particular key.
type entry struct {
	p unsafe.Pointer // *interface{}
}

entry是map中存放数据的槽位,使用entry的指针可以让read表和dirty表进行内存共享,在数据同步的时候,避免数据拷贝,只需要操作指针即可。

3.4 sync map 的结构图

在这里插入图片描述

这是一个空的sync map的结构图。

3.5 插入数据

插入数据通过Store存储:

// 插入一个key-value
func (m *Map) Store(key, value interface{}) {
    // 从 atomic.Value中拿出read表,需要做类型转换
	read, _ := m.read.Load().(readOnly)
	// 如果 插入的数据已经存在,ok 为true ,那么检测是否标记为删除,如果标记删除,那么必须先写入dirty表,维护标记,
	// 如果没有被标记删除,那么尝试进行 cas 交换新值,如果cas成功,结束,否则加锁写dirty表
	if e, ok := read.m[key]; ok && e.tryStore(&value) {
		return
	}
	// 加锁
	m.mu.Lock()
	// 重新读取read表,防止之前拿到的read表被更新,但是当前goroutine没有重新读取,导致数据丢失
	read, _ = m.read.Load().(readOnly)
	// 如果 read表中存在数据,那么进行更新
	if e, ok := read.m[key]; ok {
	    // 检查 read表中 key 是否标记删除了,如果删除,那么必须写入dirty表
		if e.unexpungeLocked() {
		    // 使用cas进行写入 key
			m.dirty[key] = e
		}
		// 使用 cas 写入 value
		e.storeLocked(&value)
	// 如果 read 表中没有,那么写入dirty表,如果dirty表中已经存在,也就是还未做数据同步就又修改了
	} else if e, ok := m.dirty[key]; ok {
	    // 直接使用 cas 写入 value
		e.storeLocked(&value)
	} else {
	// dirty表中没有key
	    // amended=true表示未同步,false表示已经同步, 如果之前数据已经同步了,那么本次是写入dirty表的第一个数据,需要初始化dirty表
		if !read.amended {
			// 尝试初始化dirty表,如果dirty表不为空,什么也不做
			m.dirtyLocked()
			// 更新read表的 amended=true 表示有数据未同步,read表的atomic.Value不变
			// 主要修改 amended 标记
			m.read.Store(readOnly{m: read.m, amended: true})
		}
		// dirty写入value
		m.dirty[key] = newEntry(value)
	}
	// 解锁
	m.mu.Unlock()
}

插入数据后的结构图:
在这里插入图片描述

3.6 查找数据

查找数据是通过Load进行查找的:

func (m *Map) Load(key interface{}) (value interface{}, ok bool) {
    // 获取read表
	read, _ := m.read.Load().(readOnly)
	// 查询 read表
	e, ok := read.m[key]
	// 如果 read表没有找到,或者有数据未同步
	if !ok && read.amended {
	    // 加锁
		m.mu.Lock()
		// 重新加载 read 表
		read, _ = m.read.Load().(readOnly)
		// 查询read表
		e, ok = read.m[key]
		// read 表没有找到,或者有数据未同步
		if !ok && read.amended {
		    // 查询dirty表
			e, ok = m.dirty[key]
			// misses++,如果misses>= read表,那么用dirty表替换read表,并重置 misses值
			m.missLocked()
		}
		// 解锁
		m.mu.Unlock()
	}
	// 如果 read表未找到,而且数据全部都同步了,那么说明key不存在,返回nil
	if !ok {
		return nil, false
	}
	// 否则返回 key 对应的value,如果entry被标记删除了,返回nil
	return e.load()
}

对于上面的结构图,因为amended=true,那么就会在dirty表中查询,每次查询都会misses++,等遍历完了,或者misses等于dirty表size,那么使用dirty表替换read表。
在这里插入图片描述

如果使用dirty表替换了read表,dirty表会置空nil:

func (m *Map) missLocked() {
	m.misses++
	if m.misses < len(m.dirty) {
		return
	}
	// read 表使用dirty表替换
	m.read.Store(readOnly{m: m.dirty})
	// dirty表置空
	m.dirty = nil
	m.misses = 0
}

3.7 再次插入

因为在查询的时候,已经将dirty给了read表,那么在次插入的时候,如果在read表中找到了,那么使用cas修改read表。
如果没有找到,那么将read表同步到dirty表,然后插入数据,amended=true.
还记的插入数据里面的这个代码吗:
在这里插入图片描述

func (m *Map) dirtyLocked() {
    // 如果dirty表不为空结束
	if m.dirty != nil {
		return
	}
	// 如果dirty表为空,那么拷贝read表的map到dirty,数据通过指针共享
	read, _ := m.read.Load().(readOnly)
	m.dirty = make(map[interface{}]*entry, len(read.m))
	for k, e := range read.m {
		if !e.tryExpungeLocked() {
			m.dirty[k] = e
		}
	}
}

所以再次插入后的结构图:
在这里插入图片描述

为什么dirty表需要冗余read表的数据,直接存储增量数据不行吗?

dirty表通过冗余read表中的数据从而维护一个全量数据,read表只是dirty表的一个临时副本。数据同步的时候,直接用dirty表替换read表,避免遍历。
同时在删除数据的时候,只是标记删除,由dirty表进行执行,避免多个入口进行删除数据,从而导致数据不一致,在dirty表从read表拉取数据的时候,会忽略标记删除的数据。

3.8 删除数据

删除操作通过Delete实现:

func (m *Map) Delete(key interface{}) {
    // delete真正由LoadAndDelete实现
	m.LoadAndDelete(key)
}

func (m *Map) LoadAndDelete(key interface{}) (value interface{}, loaded bool) {
    // 获取read表
	read, _ := m.read.Load().(readOnly)
	// 从读表中查询
	e, ok := read.m[key]
	// 没有找到而且存在数据未同步
	if !ok && read.amended {
	    // 加锁
		m.mu.Lock()
		// 重新读取read表
		read, _ = m.read.Load().(readOnly)
		// 从read表中查找
		e, ok = read.m[key]
		// 没有找到而且从在数据未同步
		if !ok && read.amended {
		    // 从dirty表中查找
			e, ok = m.dirty[key]
			// 删除dirty表数据
			delete(m.dirty, key)
			// misses++,如果misses大于等于dirty表数量,将dirty表转移到read表,dirty表置空
			m.missLocked()
		}
		// 解锁
		m.mu.Unlock()
	}
	// 如果找到了,将key的value指针清空(因为read表是原生map实现,需要避免读写冲突)
	if ok {
		return e.delete()
	}
	// 如果没有找到,返回false
	return nil, false
}

因为原生map不支持并发读写,为了避免读写冲突,那么从read表中删除数据时,只是将原生map对应的key的value置空,下次dirty表从read表拉取数据的时候,忽略value的entry为空的key.
当删除数据后的结构图如下:
在这里插入图片描述

dirty表从read表拉取数据,跳过value的entry为空的逻辑如下:
在这里插入图片描述

使用cas,设置旧值为nil,来判断是否为nil,如果是nil ,返回true,跳过key的拷贝。

4. 总结

sync map 将互斥锁内置实现并发读写,将互斥锁的范围仅仅限定在dirty表,减少锁等待和锁调用,提升性能。因为dirty表和read表总是在争取保持一致,所以大部分读场景下,read表就能查询到数据,适合读多写少。
sync map 中read表和dirty表都会持有key,所以内存占用上会比较大,而且在写多读少的场景下,因为既要遍历read表,又要遍历dirty表,性能上会比较慢。

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

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

相关文章

Unity基础学习

目录 基础知识点3D数学——基础Mathf三角函数坐标系 3D数学——向量向量模长和单位向量向量的加减乘除向量点乘向量叉乘向量插值运算 3D数学——四元数为何使用四元数四元数是什么四元数常用方法四元数计算 MonoBehavior中的重要内容延迟函数协同程序协同程序原理 Resources资源…

谁用过腾讯云轻量应用服务器2核2G3M配置,支持多少人在线?

腾讯云轻量应用服务器2核4G5M配置一年优惠价165元、252元15个月、三年756元&#xff0c;100%CPU性能&#xff0c;5M带宽下载速度640KB/秒&#xff0c;60GB SSD系统盘&#xff0c;月流量500GB&#xff0c;折合每天16.6GB流量&#xff0c;超出月流量包的流量按照0.8元每GB的价格支…

uniapp实现点击标签文本域中显示标签内容

先上一个效果图 实现的效果有&#xff1a; ①.点击标签时&#xff0c;标签改变颜色并处于可删除状态 ②.切换标签&#xff0c;文本域中出现标签的内容 ③.点击标签右上角的删除可删掉标签&#xff0c;同时清除文本域中标签的内容 ④.可输入内容&#xff0c;切换时不影响输入…

系统设计学习(三)限流与零拷贝

七、有哪些常用限流算法&#xff1f; Leaky Bucket 漏桶 漏桶可理解为是一个限定容量的请求队列。想象有一个桶&#xff0c;有水&#xff08;指请求或数据&#xff09;从上面流进来&#xff0c;水从桶下面的一个孔流出来。水流进桶的速度可以是随机的&#xff0c;但是水流出桶…

蓝桥杯小白赛第 7 场 3.奇偶排序(sort排序 + 双数组)

思路&#xff1a;在第一次看到这道题的时候我第一想法是用冒泡&#xff0c;但好像我的水平还不允许我写出来。我又读了遍题目发现它的数据很小&#xff0c;我就寻思着把它分成奇偶两部分。应该怎么分呢&#xff1f; 当然在读入的时候把这个问题解决就最好了。正好它的数据范围…

前端项目(vue3)自动化部署(Gitlab CI/CD)

天行健&#xff0c;君子以自强不息&#xff1b;地势坤&#xff0c;君子以厚德载物。 每个人都有惰性&#xff0c;但不断学习是好好生活的根本&#xff0c;共勉&#xff01; 文章均为学习整理笔记&#xff0c;分享记录为主&#xff0c;如有错误请指正&#xff0c;共同学习进步。…

C语言练习题【复试准备】

1、BoBo教KiKi字符常量或字符变量表示的字符在内存中以ASCII码形式存储。BoBo出了一个问题给KiKi&#xff0c;转换以下ASCII码为对应字符并输出他们。 //73,32,99,97,110,32,100,111,32,105,116,33 int main() {int arr[] {73,32,99,97,110,32,100,111,32,105,116,33};int i …

Swift:.ignoresSafeArea():自由布局的全方位掌握

ignoresSafeArea(_ regions : edges:)修饰符的说明 SwiftUI布局系统会调整视图的尺寸和位置&#xff0c;以避免特定的安全区域。这就确保了系统内容&#xff08;比如软件键盘&#xff09;或设备边缘不会遮挡您的视图。要将您的内容扩展到这些区域&#xff0c;您可以通过应用该修…

一文看懂红帽认证含金量有多高!

近期好多人来问红帽认证&#xff0c;有些是还在校的大学生&#xff0c;有些是已经工作的运维小伙伴。!现在的就业和职场环境下&#xff0c;系统学Linux确实是非常必要的。今天就给大家详细介绍下红帽认证&#xff0c;看看它的含金量有多高! 红帽认证是什么?红帽认证等级?红帽…

Sublime查看ANSI编码文档乱码问题

原因为没有安装对应的解码插件。 选择安装插件包 选择插件包&#xff1a;ConvertToUTF8或者GBK&#xff0c;我试了第一个插件包不行&#xff0c;安装GBK插件包后OK。

AI车辆占道识别摄像机

随着城市车辆数量的增加和交通状况的复杂化&#xff0c;道路交通安全问题日益突出&#xff0c;其中车辆占道现象严重影响了交通秩序和道路通畅。为了有效监管和防范车辆占道行为&#xff0c;AI车辆占道识别摄像机应运而生&#xff0c;利用人工智能技术&#xff0c;实现对车辆占…

探索CorelDRAW软件2024最新中文版的强大魅力,让你的电脑数码设计更上一层楼!

在当今日益发展的数字化时代&#xff0c;设计已成为连接创意与现实之间的桥梁&#xff0c;而CorelDRAW软件则是设计师们手中的得力助手。特别是随着CorelDRAW 2024最新中文版的发布&#xff0c;这一设计工具的魅力和功能得到了进一步的提升&#xff0c;为广大设计师们提供了前所…

strcmp的模拟实现

一&#xff1a;strcmp函数的定义&#xff1a; strcmp函数功能的解释&#xff1a; 比较两个字符串的大小&#xff08;按照字符串中字符的ascll码值&#xff09;。 标准规定&#xff1a; 第一个字符串大于第二个字符串&#xff0c;则返回大于 0 的数字 第一个字符串等于第二个…

Redis持久化和集群

redis持久化 RDB方式 Redis Database Backup file (redis数据备份文件), 也被叫做redis数据快照. 简单来说就是把内存中的所有数据记录到磁盘中. 快照文件称为RDB文件, 默认是保存在当前运行目录. [rootcentos-zyw ~]# docker exec -it redis redis-cli 127.0.0.1:6379> sav…

每日一题 第四期 洛谷 查找文献

【深基18.例3】查找文献 链接 题目描述 小 K 喜欢翻看洛谷博客获取知识。每篇文章可能会有若干个&#xff08;也有可能没有&#xff09;参考文献的链接指向别的博客文章。小 K 求知欲旺盛&#xff0c;如果他看了某篇文章&#xff0c;那么他一定会去看这篇文章的参考文献&…

Mybatis sql 控制台格式化

package com.mysql; import org.apache.commons.lang.StringUtils; import org.apache.ibatis.logging.Log;import java.util.*;/*** Description: sql 格式化* Author: DingQiMing* Date: 2023-07-17* Version: V1.0*/ public class StdOutImpl implements Log {private stati…

石子合并多种解法

线性朴素n^3 using ll long long;int dp1[305][305], dp2[305][305]; int main() {int n;std::cin >> n;std::vector<int>a(n 1), sum(n 1);//扩增,计算前缀和for (int i 1; i < n; i) {std::cin >> a[i];sum[i] sum[i - 1] a[i];}//i是区间for (i…

洛谷P1182数列分段

题目描述 对于给定的一个长度为 N 的正整数数列 &#xff0c;现要将其分成 M&#xff08;M≤N&#xff09;段&#xff0c;并要求每段连续&#xff0c;且每段和的最大值最小。 关于最大值最小&#xff1a; 例如一数列 4 2 4 5 14 2 4 5 1 要分成 33 段。 将其如下分段&#…

攻防世界-CatchCat

题目&#xff1a;附件为 分析题目&#xff0c;可知文件里面是一堆关于GPS的数据&#xff0c;所以我们将GPS的轨迹绘制出来&#xff08;GPS地图绘制网站&#xff1a;GPS Visualizer&#xff1a;从 GPS 数据文件绘制地图&#xff09; 将文件导入后绘制地图&#xff0c;得到如图&a…

两道简单却实用的python面试题

题目一&#xff1a;python中String类型和unicode什么关系 整理答案&#xff1a;string是字节串&#xff0c;而unicode是一个统一的字符集&#xff0c;utf-8是它的一种存储实现形式&#xff0c;string可为utf-8编码&#xff0c;也可编码为GBK等各种编码格式 题目二&#xff1a…