返回顶部
首页 > 资讯 > 精选 >如何理解Go里面的sync.Map
  • 341
分享到

如何理解Go里面的sync.Map

2023-06-19 09:06:40 341人浏览 安东尼
摘要

本篇文章为大家展示了如何理解Go里面的sync.Map,内容简明扼要并且容易理解,绝对能使你眼前一亮,通过这篇文章的详细介绍希望你能有所收获。基础筑基在大多数语言中原始map都不是一个线程安全的数据结构,那如果要在多个线程或者gorouti

本篇文章为大家展示了如何理解Go里面的sync.Map,内容简明扼要并且容易理解,绝对能使你眼前一亮,通过这篇文章的详细介绍希望你能有所收获。

基础筑基

在大多数语言中原始map都不是一个线程安全数据结构,那如果要在多个线程或者goroutine中对线程进行更改就需要加,除了加1个大锁,不同的语言还有不同的优化方式, 像在java和go这种语言其实都采用的是链表法来进行map的实现。

并发安全的map实现的三种方式

在go语言中实现多个goroutine并发安全访问修改的map的方式,主要有如下三种:

实现方式原理适用场景
map+Mutex通过Mutex互斥锁来实现多个goroutine对map的串行化访问读写都需要通过Mutex加锁和释放锁,适用于读写比接近的场景
map+RWMutex通过RWMutex来实现对map的读写进行读写锁分离加锁,从而实现读的并发性能提高同Mutex相比适用于读多写少的场景
sync.Map底层通分离读写map和原子指令来实现读的近似无锁,并通过延迟更新的方式来保证读的无锁化读多修改少,元素增加删除频率不高的情况,在大多数情况下替代上述两种实现

上面三种实现具体的性能差异可能还要针对不同的具体的业务场景和平台、数据量等因此来进行综合的测试源码学习更多的是了解其实现细节,以便在出现性能瓶颈的时候可以进行分析,找出解决解决方案

map的容量问题

如何理解Go里面的sync.Map 在Mutex和RWMutex实现的并发安全的map中map随着时间和元素数量的增加、删除,容量会不断的递增,在某些情况下比如在某个时间点频繁的进行大量数据的增加,然后又大量的删除,其map的容量并不会随着元素的删除而缩小,而在sync.Map中,当进行元素从dirty进行提升到read map的时候会进行重建,可能会缩容

无锁读与读写分离

如何理解Go里面的sync.Map

读写分离

并发访问map读的主要问题其实是在扩容的时候,可能会导致元素被hash到其他的地址,那如果我的读的map不会进行扩容操作,就可以进行并发安全的访问了,而sync.map里面正是采用了这种方式,对增加元素通过dirty来进行保存

无锁读

通过read只读和dirty写map将操作分离,其实就只需要通过原子指令对read map来进行读操作而不需要加锁了,从而提高读的性能

写加锁与延迟提升

如何理解Go里面的sync.Map

写加锁

上面提到增加元素操作可能会先增加大dirty写map中,那针对多个goroutine同时写,其实就需要进行Mutex加锁了

延迟提升

上面提到了read只读map和dirty写map, 那就会有个问题,默认增加元素都放在dirty中,那后续访问新的元素如果都通过 mutex加锁,那read只读map就失去意义,sync.Map中采用一直延迟提升的策略,进行批量将当前map中的所有元素都提升到read只读map中从而为后续的读访问提供无锁支持

指针与惰性删除

如何理解Go里面的sync.Map

map里面的指针

map里面存储数据都会涉及到一个问题就是存储值还是指针,存储值可以让 map作为一个大的的对象,减轻垃圾回收的压力(避免扫描所有小对象),而存储指针可以减少内存利用,而sync.Map中其实采用了指针结合惰性删除的方式,来进行 map的value的存储

惰性删除

惰性删除是并发设计中一中常见的设计,比如删除某个个链表元素,如果要删除则需要修改前后元素的指针,而采用惰性删除,则通常只需要给某个标志位设定为删除,然后在后续修改中再进行操作,sync.Map中也采用这种方式,通过给指针指向某个标识删除的指针,从而实现惰性删除

源码分析

数据结构分析

Map

type Map struct {    mu Mutex    // read是一个readOnly的指针,里面包含了一个map结构,就是我们说的只读map对该map的元素的访问    // 不需要加锁,只需要通过atomic加载最新的指针即可    read atomic.Value // readOnly    // dirty包含部分map的键值对,如果要访问需要进行mutex获取    // 最终dirty中的元素会被全部提升到read里面的map中    dirty map[interface{}]*entry   // misses是一个计数器用于记录从read中没有加载到数据    // 尝试从dirty中进行获取的次数,从而决定将数据从dirty迁移到read的时机    misses int}

readOnly

只读map,对该map元素的访问不需要加锁,但是该map也不会进行元素的增加,元素会被先添加到dirty中然后后续再转移到read只读map中,通过atomic原子操作不需要进行锁操作

type readOnly struct {    // m包含所有只读数据,不会进行任何的数据增加和删除操作    // 但是可以修改entry的指针因为这个不会导致map的元素移动    m       map[interface{}]*entry    // 标志位,如果为true则表明当前read只读map的数据不完整,dirty map中包含部分数据    amended bool }

entry

entry是sync.Map中值得指针,如果当p指针指向expunged这个指针的时候,则表明该元素被删除,但不会立即从map中删除,如果在未删除之前又重新赋值则会重用该元素

type entry struct {// 指向元素实际值得指针    p unsafe.Pointer // *interface{}}

数据的存储

如何理解Go里面的sync.Map

2.2.1 元素存在只读map

元素如果存储在只读map中,则只需要获取entry元素,然后修改其p的指针指向新的元素就可以了,因为是原地操作所以map不会发生变化

    read, _ := m.read.Load().(readOnly)    if e, ok := read.m[key]; ok && e.tryStore(&value) {        return    }

元素存在进行变更后的只读map中

如果此时发现元素存在只读 map中,则证明之前有操作触发了从dirty到read map的迁移,如果此时发现存在则修改指针即可

    read, _ = m.read.Load().(readOnly)    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.            // 如果key之前已经被删除,则这个地方会将key从进行nil覆盖之前已经删除的指针            // 然后将它加入到dirty中            m.dirty[key] = e        }        // 调用atomic进行value存储        e.storeLocked(&value)    }

元素存在dirty map中

如果元素存在dirty中其实同read map逻辑一样,只需要修改对应元素的指针即可

    } else if e, ok := m.dirty[key]; ok {        // 如果已经在dirty中就会直接存储        e.storeLocked(&value)    } else {

元素之前不存在

如果元素之前不存在当前Map中则需要先将其存储在dirty map中,同时将amended标识为true,即当前read中的数据不全,有一部分数据存储在dirty中

        // 如果当前不是在修正状态        if !read.amended {                           // 新加入的key会先被添加到dirty map中, 并进行read标记为不完整            // 如果dirty为空则将read中的所有没有被删除的数据都迁移到dirty中            m.dirtyLocked()            m.read.Store(readOnly{m: read.m, amended: true})        }        m.dirty[key] = newEntry(value)

数据迁移

read map到dirty map的迁移

如何理解Go里面的sync.Map 在刚初始化和将所有元素迁移到read中后,dirty默认都是nil元素,而此时如果有新的元素增加,则需要先将read map中的所有未删除数据先迁移到dirty中

func (m *Map) dirtyLocked() {    if m.dirty != nil {        return    }    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 map的迁移

如何理解Go里面的sync.Map 当持续的从read访问穿透到dirty中后,就会触发一次从dirty到read的迁移,这也意味着如果我们的元素读写比差比较小,其实就会导致频繁的迁移操作,性能其实可能并不如rwmutex等实现

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}

数据加载

如何理解Go里面的sync.Map

只读无锁

Load数据的时候回先从read中获取,如果此时发现元素,则直接返回即可

   read, _ := m.read.Load().(readOnly)    e, ok := read.m[key]

加锁读取read和dirty

加锁后会尝试从read和dirty中读取,同时进行misses计数器的递增,如果满足迁移条件则会进行数据迁移

       read, _ = m.read.Load().(readOnly)        e, ok = read.m[key]        if !ok && read.amended {            e, ok = m.dirty[key]            // 这里将采取缓慢迁移的策略            // 只有当misses计数==len(m.dirty)的时候,才会将dirty里面的数据全部晋升到read中            m.missLocked()        }

数据删除

如何理解Go里面的sync.Map 数据删除则分为两个过程,如果数据在read中,则就直接修改entry的标志位指向删除的指针即可,如果当前read中数据不全,则需要进行dirty里面的元素删除尝试,如果存在就直接从dirty中删除即可

func (m *Map) Delete(key interface{}) {    read, _ := m.read.Load().(readOnly)    e, ok := read.m[key]    if !ok && read.amended {        m.mu.Lock()        read, _ = m.read.Load().(readOnly)        e, ok = read.m[key]        if !ok && read.amended {            delete(m.dirty, key)        }        m.mu.Unlock()    }    if ok {        e.delete()    }}

mutex与加锁后的read map重复读

因为mutex互斥的是所有操作,包括dirty map的修改、数据的迁移、删除,如果在进行m.lock的时候,已经有一个提升dirty到read操作在进行,则执行完成后dirty实际上是没有数据的,所以此时要再次进行read的重复读 

上述内容就是如何理解Go里面的sync.Map,你们学到知识或技能了吗?如果还想学到更多技能或者丰富自己的知识储备,欢迎关注编程网精选频道。

--结束END--

本文标题: 如何理解Go里面的sync.Map

本文链接: https://lsjlt.com/news/295213.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

猜你喜欢
  • 如何理解Go里面的sync.Map
    本篇文章为大家展示了如何理解Go里面的sync.Map,内容简明扼要并且容易理解,绝对能使你眼前一亮,通过这篇文章的详细介绍希望你能有所收获。基础筑基在大多数语言中原始map都不是一个线程安全的数据结构,那如果要在多个线程或者gorouti...
    99+
    2023-06-19
  • 如何理解Go里面的互斥锁mutex
    如何理解Go里面的互斥锁mutex,相信很多没有经验的人对此束手无策,为此本文总结了问题出现的原因和解决方法,通过这篇文章希望你能解决这个问题。1. 锁的基础概念1.1 CAS与轮询1.1.1 cas实现锁 在锁的实现中现在越来越多的采用C...
    99+
    2023-06-19
  • Go使用sync.Map来解决map的并发操作问题
    目录前言 map 并发操作出现问题 sync.Map 解决并发操作问题 计算 map 长度 计算 sync.Map 长度 前言 在 Golang 中 map 不是并发安全的,自 1...
    99+
    2024-04-02
  • Go怎么使用sync.Map来解决map的并发操作问题
    这篇文章主要介绍“Go怎么使用sync.Map来解决map的并发操作问题”,在日常操作中,相信很多人在Go怎么使用sync.Map来解决map的并发操作问题问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”Go怎...
    99+
    2023-06-25
  • 如何在面试中展示你对Go和Linux的理解?
    在当前的技术行业中,越来越多的公司开始使用Go语言和Linux系统进行开发和部署。因此,如果你想在这个领域找到一份工作,对于这两个技术的深入理解是非常重要的。在面试过程中,你需要展示你对Go和Linux的理解,让面试官相信你是一位合适的候选...
    99+
    2023-08-22
    linux 响应 面试
  • Go语言文档解析:sync.Map函数实现并发安全的映射
    Go语言是一款近年来备受前端开发人员喜爱的编程语言。其中,sync.Map函数是旨在实现并发安全的映射,可以帮助开发者解决在高并发下出现的数据访问问题。本文将介绍sync.Map函数的使用方法,并提供具体的代码示例。一、sync.Map函数...
    99+
    2023-11-04
    Go语言 文档解析 syncMap
  • JavaScript面试必备:如何处理Go的响应?
    在JavaScript开发中,我们经常需要和后端交互,而Go语言是近年来备受关注的后端语言之一。在和Go语言后端进行交互的过程中,处理响应是非常重要的一部分。在本文中,我们将探讨如何使用JavaScript处理Go语言的响应。 使用fe...
    99+
    2023-11-06
    响应 面试 javascript
  • 面试中如何展现你对Go接口和函数的理解?
    在Go语言中,接口(interface)是一种类型,它定义了一组方法的签名。接口类型可以定义为一个变量的类型,因此它可以被用来声明变量、作为函数参数或返回值。在Go语言中,接口是非常重要的,因为它允许我们编写可复用的代码,同时还可以减少代...
    99+
    2023-06-16
    接口 函数 面试
  • 如何在JavaScript面试中展示对Go语言并发的理解?
    在现代的软件开发领域,面试已经成为了招聘流程中不可或缺的一部分。在JavaScript面试中,对Go语言并发的理解是一项重要的技能,而如何在面试中展示这种理解则是一项挑战。在本文中,我们将介绍如何在JavaScript面试中展示对Go语言并...
    99+
    2023-06-23
    并发 javascript 面试
  • 如何理解golang里面的读写锁实现与核心原理
    如何理解golang里面的读写锁实现与核心原理,很多新手对此不是很清楚,为了帮助大家解决这个难题,下面小编将为大家详细讲解,有这方面需求的人可以来学习下,希望你能有所收获。基础筑基读写锁的特点读写锁区别与互斥锁的主要区别就是读锁之间是共享的...
    99+
    2023-06-19
  • 如何理解Go运行时中的Mutex
    这篇文章主要讲解了“如何理解Go运行时中的Mutex”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“如何理解Go运行时中的Mutex”吧!sync.Mutex...
    99+
    2024-04-02
  • 如何理解Go中的类型别名
    这篇文章主要介绍“如何理解Go中的类型别名”,在日常操作中,相信很多人在如何理解Go中的类型别名问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”如何理解Go中的类型别名”的疑惑有所帮助!接下来,请跟着小编一起来...
    99+
    2023-06-15
  • 如何理解Go语言的模块化
    本篇内容介绍了“如何理解Go语言的模块化”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!环境变量学习 go 语言的第一步,当然是安装以及环境变...
    99+
    2023-06-15
  • 如何彻底理解Go指针
    如何彻底理解Go指针,很多新手对此不是很清楚,为了帮助大家解决这个难题,下面小编将为大家详细讲解,有这方面需求的人可以来学习下,希望你能有所收获。什么是指针简单点说,指针是指向另一个地址的值。这是教科书上的解释,但如果你转自一门不用谈论变量...
    99+
    2023-06-16
  • 如何理解JS中的面向对象
    这篇文章主要讲解了“如何理解JS中的面向对象”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“如何理解JS中的面向对象”吧!在讲这个之前我们先来说说类,了解面向...
    99+
    2024-04-02
  • GO语言学习笔记:如何在面试中展示您对Laravel的理解?
    在现代Web开发中,Laravel是一个非常受欢迎的PHP框架。但是,如果您是一名GO语言开发人员,您可能会想知道如何在面试中展示您对Laravel的理解。在本文中,我们将探讨一些GO语言开发人员可以使用的技巧,以便在面试中展示对Lara...
    99+
    2023-10-24
    学习笔记 面试 laravel
  • 如何在Go语言面试中展示你对二维码和索引的理解?
    在Go语言面试中,展示自己对于二维码和索引的理解是非常重要的。这两个概念是在Go语言编程中经常用到的,因此掌握它们对于一个程序员来说是非常必要的。 二维码是一种矩阵式的条码,它可以存储大量的信息,并且可以被快速扫描读取。在Go语言中,我们可...
    99+
    2023-07-03
    二维码 索引 面试
  • 如何在面试中展示你对Go和NumPy文件操作的深入理解?
    在现代的软件工程中,Go和NumPy是两个非常流行的编程语言。其中,Go通常用于构建高效且并发性能较好的Web应用程序,而NumPy则用于科学计算和数据分析。如果你正在寻找一份与这两种语言相关的工作,那么你需要了解如何在面试中展示你对Go和...
    99+
    2023-11-13
    numy 文件 面试
  • 深入理解Go语言的面向对象模型
    go 语言通过对象、类型和接口提供面向对象编程(oop)支持。对象是匿名类型,类型是隐式类,接口定义对象行为,而继承则通过类型嵌入实现。实战案例:使用 oop 创建一个学生管理系统,其中...
    99+
    2024-04-04
    go语言 面向对象
  • Go语言面试题:如何处理存储关键字?
    在Go语言中,关键字是指被Go语言保留用于特定用途的标识符,如if、else、for等等。在编写代码时,我们需要注意到关键字的使用,以避免出现语法错误和运行时错误。而在存储关键字时,我们需要考虑如何处理这些特殊的标识符。本篇文章将介绍如何...
    99+
    2023-08-19
    关键字 面试 存储
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作