map、sync.map、concurrent-map适用场景与源码解析

news2024/11/22 11:52:31

最近一直加班,无论工作日还是周末,虽然每天很忙但总感觉空空的,很少有时间停下来思考与总结。项目中各种甩锅,最后最苦逼的还是落到了研发的头上,文档编写、环境部署、问题排查虐得一遍又一遍。事情杂乱,研发效率超级低,不知道何是是个头呀

背景

在go中,map是最常用的集合之一。
其底层key存储采用的是hash算法,在数据检索时提供了强大的性能,深受各大开发者喜爱。

但在并发方面,map则存在较为严重的问题。一不留神,就会导致整个程序出错退出。

为了解决map并发操作的问题,诞生出了sync.map及第三方的concurrent-map等集合。
那么哪个集合更符合我们的需求,就需要都来了解一下。

map线程安全问题

先来看一下原生map在并发操作上带来的问题,随便写个测试代码验证一下:

	var myMap = make(map[int]int, 0)
	for i := 0; i < 10; i++ {
		go func() {
			for x := 0; x < 1000; x++ {
				time.Sleep(time.Millisecond)
				myMap[x] = x + 1
				fmt.Printf("x=%d m=%d\n", x, myMap[x])
			}
		}()
	}
	time.Sleep(time.Second * 5)

可能出现如下错误:

x=12 m=13
fatal error: concurrent map writes
x=12 m=13

goroutine 27 [running]:

fatal error: concurrent map writes

如上面所示,使用map在并发场景的情况下进行并发读写,程序可能抛出以上错误则会导致程序退出

给map加互斥锁(Mutex)

为了解决以上问题,在代码量改动比较上的情况下则是加上lock锁;
代码改造为:

var myMap = make(map[int]int, 0)
	var lock sync.Mutex
	for i := 0; i < 10; i++ {
		go func() {
			lock.Lock()
			for x := 0; x < 1000; x++ {
				time.Sleep(time.Millisecond)
				myMap[x] = x + 1
				fmt.Printf("x=%d m=%d\n", x, myMap[x])
			}
			lock.Unlock()
		}()
	}
	time.Sleep(time.Second * 5)

所有协程都使用同一把lock锁,进行数据读写时先获取锁再执行对map的读写操作。
这种方式对于并发较小的场景一般也能进行处理,对于并发大时则可能会出现耗时过久才能获取锁。

给map加读写锁(RWMutex)

针对上面给map加一把大锁,如果带来了性能不佳的情况,且应用场景为比较明确的读多写少场景的场景,可以进一步优化为读写锁(RWMutex)分离实现,编码上来看也还过得去。

sync.map

下面深入了解一下sync.map是如何解决的golang中map并发安全问题的。

将原代码进行修改:

	var myMap = sync.Map{}
	for i := 0; i < 10; i++ {
		go func() {
			for x := 0; x < 1000; x++ {
				time.Sleep(time.Millisecond)
				myMap.Store(x, x+1)
				value, _ := myMap.Load(x)
				fmt.Printf("x=%d m=%d\n", x, value)
			}
		}()
	}
	time.Sleep(time.Second * 5)

再看来一下sync.map的源码,代码行数总体不到600行,与go的代码的简短精悍比较符合。

sync.map结构

在了解sync.map的源码前,非常有必要了解一下sync.map的数据存储结构。

type Map struct {
	mu Mutex

	read atomic.Pointer[readOnly]

	dirty map[any]*entry

	misses int
}

type readOnly struct {
	m       map[any]*entry
	amended bool // true if the dirty map contains some key not in m.
}

type entry struct {
	p atomic.Pointer[any]
}

如下图:
sync.map

Store流程

sync.map中每个kv对的新增使用store方法实现。源码如下:

// Store sets the value for a key.
func (m *Map) Store(key, value any) {
	_, _ = m.Swap(key, value)
}

// Swap swaps the value for a key and returns the previous value if any.
// The loaded result reports whether the key was present.
func (m *Map) Swap(key, value any) (previous any, loaded bool) {
	read := m.loadReadOnly()
	if e, ok := read.m[key]; ok {
		if v, ok := e.trySwap(&value); ok {
			if v == nil {
				return nil, false
			}
			return *v, true
		}
	}

	m.mu.Lock()
	read = m.loadReadOnly()
	if e, ok := read.m[key]; ok {
		if e.unexpungeLocked() {
			// The entry was previously expunged, which implies that there is a
			// non-nil dirty map and this entry is not in it.
			m.dirty[key] = e
		}
		if v := e.swapLocked(&value); v != nil {
			loaded = true
			previous = *v
		}
	} else if e, ok := m.dirty[key]; ok {
		if v := e.swapLocked(&value); v != nil {
			loaded = true
			previous = *v
		}
	} else {
		if !read.amended {
			// We're adding the first new key to the dirty map.
			// Make sure it is allocated and mark the read-only map as incomplete.
			m.dirtyLocked()
			m.read.Store(&readOnly{m: read.m, amended: true})
		}
		m.dirty[key] = newEntry(value)
	}
	m.mu.Unlock()
	return previous, loaded
}

流程图如下:
store

在store中主要分为2个分支:

  1. 更新的key在read中存在,使用自旋锁(CAS)的方式对value进行更新
  2. 将更新的kv对在dirty中进行更新,并确保dirty初始化完毕且amended标识为true

Load流程

sync.map中在kv对被存储后,就可以使用Load方法查询了。
其源码如下:

// Load returns the value stored in the map for a key, or nil if no
// value is present.
// The ok result indicates whether value was found in the map.
func (m *Map) Load(key any) (value any, ok bool) {
	read := m.loadReadOnly()
	e, ok := read.m[key]
	if !ok && read.amended {
		m.mu.Lock()
		// Avoid reporting a spurious miss if m.dirty got promoted while we were
		// blocked on m.mu. (If further loads of the same key will not miss, it's
		// not worth copying the dirty map for this key.)
		read = m.loadReadOnly()
		e, ok = read.m[key]
		if !ok && read.amended {
			e, ok = m.dirty[key]
			// Regardless of whether the entry was present, record a miss: this key
			// will take the slow path until the dirty map is promoted to the read
			// map.
			m.missLocked()
		}
		m.mu.Unlock()
	}
	if !ok {
		return nil, false
	}
	return e.load()
}

流程图如下:
load

在了解了Store的流程后再来看Load就比较简单啦。
主要流程为:

  1. 从readOnly中查询key的值
  2. readOnly中无法找到,且有新的值被存到map中(amended为true)
    1. 从dirty中查找
    2. 将misses自增
    3. misses大于等于dirty的数量时,将dirty设为readOnly;并重置dirty与misses

需要重点关注的关于dirtry升级为readOnly的代码如下:

func (m *Map) missLocked() {
	m.misses++
	if m.misses < len(m.dirty) {
		return
	}
	m.read.Store(&readOnly{m: m.dirty})
	m.dirty = nil
	m.misses = 0
}

Delete流程

在看了Store和Load的流程后,对sync.map的主要流程就算是基本掌握了。最后再来看一下Delete的流程:

// Delete deletes the value for a key.
func (m *Map) Delete(key any) {
	m.LoadAndDelete(key)
}
// LoadAndDelete deletes the value for a key, returning the previous value if any.
// The loaded result reports whether the key was present.
func (m *Map) LoadAndDelete(key any) (value any, loaded bool) {
	read := m.loadReadOnly()
	e, ok := read.m[key]
	if !ok && read.amended {
		m.mu.Lock()
		read = m.loadReadOnly()
		e, ok = read.m[key]
		if !ok && read.amended {
			e, ok = m.dirty[key]
			delete(m.dirty, key)
			// Regardless of whether the entry was present, record a miss: this key
			// will take the slow path until the dirty map is promoted to the read
			// map.
			m.missLocked()
		}
		m.mu.Unlock()
	}
	if ok {
		return e.delete()
	}
	return nil, false
}

Delete的流程与前面新增和查询流程类似:

  1. 先从readOnly中查询,存在对应key值调用e.delete()进行删除
  2. readOnly中不存在并被修改过(amended为true),则进行加锁从dirty中查找,并调用delete进行删除

这里需要留意的是,从readOnly中删除kv对也是使用的自旋(CAS)的方式进行删除的,源码如下:

func (e *entry) delete() (value any, ok bool) {
	for {
		p := e.p.Load()
		if p == nil || p == expunged {
			return nil, false
		}
		if e.p.CompareAndSwap(p, nil) {
			return *p, true
		}
	}
}

concurrent-map

与JAVA语言类似地,第三方的concurrent-map组件也提供了一种实现用于解决map的并发访问问题。
concurrent-map

其项目地址如下:
https://github.com/orcaman/concurrent-map

其实现方式为采用的与JAVA中的ConcurrentHashMap思路实现的,即通过多个锁提高减少对大锁竞争。

与sync.map相比,sync.map中所有的key使用同一个Mutex互斥锁,而在concurrent-map中,则存大多个Mutex互斥锁,多个key共享同一个Mutex互斥锁。

总结

在go中进行KV存储时,常用map、sync.map、concurrent-map这3种map实现。

项目中选型时具体应该使用哪个需要分析具体的业务场景,可参考sync.map中的这段话:

The Map type is optimized for two common use cases: (1) when the entry for a given key is only ever written once but read many times, as in caches that only grow, or (2) when multiple goroutines read, write, and overwrite entries for disjoint sets of keys. In these two cases, use of a Map may significantly reduce lock contention compared to a Go map paired with a separate Mutex or RWMutex.

最后,再简单总结一下:
项目中选型时,应优先考虑使用原生map进行KV存储;
多个协程的并发读写场景,应优先考虑在map中加上互斥锁(Mutex)或读写锁(RWMutex)实现,这样对map的编码方式改动也最小。
并发场景为读多写少的场景,则可考虑sync.map;如并发场景为读多写多的场景,又追求性能则也可考虑下第三方concurrent-map

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

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

相关文章

C语言之函数题

目录 1.乘法口诀表 2.交换两个整数 3.函数判断闰年 4.函数判断素数 5.计算斐波那契数 6.递归实现n的k次方 7.计算一个数的每位之和&#xff08;递归&#xff09; 8.字符串逆序&#xff08;递归实现&#xff09; 9.strlen的模拟&#xff08;递归实现&#xff09; 10.求…

信息化发展2

信息系统生命周期 1 、软件的生命周期通常包括&#xff1a;可行性分析与项目开发计划、需求分析、概要设计、详细设计、编码、测试、维护等阶段。 2 、信息系统的生命周期可以简化为&#xff1a;系统规划&#xff08;可行性分析与项目开发计划&#xff09;&#xff0c;系统分析…

【算法训练-链表】合并两个有序链表、合并K个有序链表

废话不多说&#xff0c;喊一句号子鼓励自己&#xff1a;程序员永不失业&#xff0c;程序员走向架构&#xff01;首先&#xff0c;链表对应的数据结构在这篇Blog中&#xff1a;【基本数据结构 一】线性数据结构&#xff1a;链表&#xff0c;基于对基础知识的理解来进行题目解答。…

(笔记二)利用opencv调用鼠标事件在图像上绘制图形

目录 &#xff08;1&#xff09;查看cv2所支持的鼠标事件&#xff08;2&#xff09;通过鼠标事件在图像上做标记&#xff08;3&#xff09;高级操作&#xff1a;通过移动鼠标在图像绘制图形、曲线 该功能主要创建一个鼠标事件发生时执行的回调函数。鼠标事件可以是任何与鼠标有…

配置Flink

配置flink_1.17.0 1.Flink集群搭建1.1解压安装包1.2修改集群配置1.3分发安装目录1.4启动集群、访问Web UI 2.Standalone运行模式3.YARN运行模式4.K8S运行模式 1.Flink集群搭建 1.1解压安装包 链接: 下载Flink安装包 解压文件 [gpbhadoop102 software]$ tar -zxvf flink-1.1…

前端需要理解的性能优化知识

优化的目的是展示更快、交互响应快、页面无卡顿情况。 1 性能指标 2 分析方法 使用 ChromeDevTool 作为性能分析工具来观察页面性能情况。其中Network观察网络资源加载耗时及顺序&#xff0c;Performace观察页面渲染表现及JS执行情况&#xff0c;Lighthouse对网站进行整体评分…

Linux驱动-I2C子系统基本分析

​第一&#xff1a;Linux中I2C驱动框架分析 I2C核心&#xff08;i2c_core&#xff09; I2C核心维护了i2c_bus结构体&#xff0c;提供了I2C总线驱动和设备驱动的注册、注销方法&#xff0c;维护了I2C总线的驱动、设备链表&#xff0c;实现了设备、驱动的匹配探测。此部分代码由…

CNN 01(CNN简介)

一、卷积神经网络的发展 convolutional neural network 在计算机视觉领域&#xff0c;通常要做的就是指用机器程序替代人眼对目标图像进行识别等。那么神经网络也好还是卷积神经网络其实都是上个世纪就有的算法&#xff0c;只是近些年来电脑的计算能力已非当年的那种计算水平…

sql语句中的ddl和dml

操作数据库&#xff1a;CRUD C&#xff08;create&#xff09; 创建 *数据库创建出来默认字符集为utf8 如果要更改字符集就 Create database 名称 character set gbk&#xff08;字符集&#xff09; *创建数据库&#xff1a;create database 名称 *先检查是否有该数据库在…

Python第三方库纵览

Python第三方库纵览 知识点 更广泛的Python计算生态&#xff0c;只要求了解第三方库的名称&#xff0c;不限于以下领域: 网络爬虫、数据分析、文本处理、数据可视化、用户图形界面、机器学习、Web开发、游戏开发等 知识导图 1、网络爬虫方向 网络爬虫是自动进行HTTP访问并捕…

【JAVA】实现API 接口参数签名

使用sa-tokenSpringBoot拦截器实现API 接口参数签名 在涉及跨系统接口调用时&#xff0c;我们容易碰到以下安全问题&#xff1a; 1.请求身份被伪造。 2.请求参数被篡改。 3.请求被抓包&#xff0c;然后重放攻击。 1.引入 sa-token sa-token官方文档:https://sa-token.cc/doc.ht…

HCIP-HCS华为私有云

1、概述 HCS&#xff08;HuaweiCoudStack&#xff09;华为私有云&#xff1a;6.3 之前叫FusionSphere OpenStack&#xff0c;6.3.1 版本开始叫FusionCloud&#xff0c;6.5.1 版本开始叫HuaweiCloud Stack (HCS)华为私有云软件。 开源openstack&#xff0c;发放云主机的流程&am…

第五章 树与二叉树 一、树的定义与考点

一、定义 1.树是由n (n > 0) 个节点组成的有限集合。 2.当n0时&#xff0c;称为空树。 3.在非空树中&#xff0c;有且仅有一个节点没有前驱&#xff0c;其他节点都有且仅有一个前驱&#xff0c;称为根节点。 4.每个节点有零个或多个子节点&#xff0c;而每个子节点又有零…

多态/虚函数/虚函数表

OVERVIEW 多态/虚函数/虚函数表1.虚函数引入后类发生的变化&#xff1f;2.虚函数表的生成时机和生成原因&#xff1f;3.虚函数表指针赋值的时机&#xff1f;4.类对象在内存中的布局&#xff1f;5.虚函数的工作原理和多态性的体现&#xff1f;6.其他问题 多态/虚函数/虚函数表 n…

Android JNI系列详解之生成指定CPU的库文件

一、前提 这次主要了解Android的cpu架构类型&#xff0c;以及在使用CMake工具的时候&#xff0c;如何指定生成哪种类型的库文件。 如上图所示&#xff0c;是我们之前使用CMake工具默认生成的四种cpu架构的动态库文件&#xff1a;arm64-v8a、armeabi-v7a、x86、x86_64&#xff0…

昇腾Ascend+C编程入门教程(纯干货)

2023年5月6日&#xff0c;在昇腾AI开发者峰会上&#xff0c;华为正式发布了面向算子开发场景的昇腾Ascend C编程语言。Ascend C原生支持C/C编程规范&#xff0c;通过多层接口抽象、并行编程范式、孪生调试等技术&#xff0c;极大提高了算子的开发效率&#xff0c;帮助AI开发者低…

go学习之流程控制语句

文章目录 流程控制语句1.顺序控制2.分支控制2.1单分支2.2双分支单分支和双分支的四个题目switch分支结构 3.循环控制for循环控制while 和do...while的实现 4.跳转控制语句breakcontinuegotoreturngotoreturn 流程控制语句 介绍&#xff1a;在程序中&#xff0c;程序运行的流程…

星际争霸之小霸王之小蜜蜂(七)--消失的子弹

目录 前言 一、删除子弹 二、限制子弹数量 三、继续重构代码 总结 前言 昨天我们已经让子弹飞了起来&#xff0c;但是会面临一个和之前小蜜蜂一样的问题&#xff0c;小蜜蜂的行动应该限制在窗口内&#xff0c;那么子弹也是有相同之处&#xff0c;也需要限制一个移动范围&…

智慧监狱整体解决方案PPT

导读&#xff1a;原文《智慧监狱整体解决方案PPT》&#xff08;获取来源见文尾&#xff09;&#xff0c;本文精选其中精华及架构部分&#xff0c;逻辑清晰、内容完整&#xff0c;为快速形成售前方案提供参考。 喜欢文章&#xff0c;您可以点赞评论转发本文&#xff0c;了解更多…