深入理解 Go sync.Map

news2024/11/17 23:39:42

前言

Go 语言原生 map 并不是线程安全的,要对它进行并发读写操作时,一般有两种选择:

  1. 原生map搭配Mutex或RWMutex
  2. 使用sync.Map

和原生map搭配Mutex或RWMutex相比,sync.Map在以下场景更有优势:

  • 读多写少

  • 修改,删除已存在key对应的value较多

本文将介绍sync.map的整体结构,及查,增,删,改,遍历的实现原理,以及为啥要设置expunge这个特殊值

原理

流程

sync.map的增删改查的流程大体类似,基于只读结构read,和可写结构dirty

先看key在只读结构read中是否存在,如果存在直接进行操作。否则加锁去dirty结构中检查

结构

sync.map的数据结构比较简单,涉及3个结构体:

type Map struct {
   // 锁,用于保护dirty的访问
   mu Mutex
   // 只读的map,实际存储readOnly结构体 
   read atomic.Value 
   // 可写的map
   dirty map[any]*entry
   // 从read中查询失败的次数
   misses int
}

type readOnly struct {
   m       map[any]*entry
   // 为true时,代表dirty中存在read中没有的键值对
   amended bool 
}

type entry struct {
   p unsafe.Pointer 
}
  • entry.p

    • 一般存储某个key对于的value值
    • 同时也有两个特殊的取值:nilexpunged,的Delete操作有关,后面详细介绍
  • read和dirty中,相同key底层引用了同一个entry,因此对read中的entry修改,也会影响到dirty

在这里插入图片描述

下面分析sync.Map关键方法的代码细节

Load

func (m *Map) Load(key any) (value any, ok bool) {
   read, _ := m.read.Load().(readOnly)
   e, ok := read.m[key]
   // 如果key在read中不存在,且dirty数据比read多,则去dirty中找
   if !ok && read.amended {
      m.mu.Lock()
      // 双重检查,再去read中找一次
      read, _ = m.read.Load().(readOnly)
      e, ok = read.m[key]
      // 如果read中还是没有,就去dirty中找
      if !ok && read.amended {
         e, ok = m.dirty[key]
         m.missLocked()
      }
      m.mu.Unlock()
   }
   if !ok {
      return nil, false
   }
   // 如果read中有该key,返回该value。从代码可读性角度来说,其实这一步可以在第4行直接返回
   return e.load()
}

Load整体流程为:

  • 先从read中尝试获取,如果存在直接返回

  • 否则加锁,再次从read中获取一次

    • 这里是经典的双重检查做法,在sync.Map中大量使用。因为在从read读和加锁期间,可能有其他线程对map进行了操作,使read中有该键值对了
  • 如果还是没有,就从dirty中获取
  • 在missLocked方法中,不管是否获取成功都对m.misses++,如果达到阈值,就将dirty提升为read

    • 提升dirty的目的:将全量的数据提升到read中,使得后续的操作能在read中完成,无需加锁

其中涉及到的子方法:

func (m *Map) missLocked() {
   // read中没有的次数++
   m.misses++
   // 若misses不够多,直接返回
   if m.misses < len(m.dirty) {
      return
   }
   // 否则重建read,做法为将dirty赋值给read,并将dirty,misses置空
   m.read.Store(readOnly{m: m.dirty})
   m.dirty = nil
   m.misses = 0
}
func (e *entry) load() (value any, ok bool) {
   p := atomic.LoadPointer(&e.p)
   if p == nil || p == expunged {
      return nil, false
}
   return *(*any)(p), true
}
  • entry.load()即检查entry.p是否为nil或expunged,如果是说明键值对已经被删除,返回空

Store

func (m *Map) Store(key, value any) {
   read, _ := m.read.Load().(readOnly)
   // 如果read中存在该键值对,cas更新其value
   if e, ok := read.m[key]; ok && e.tryStore(&value) {
      return
   }
   // 接下来就是当前时刻read中没有该键值对的逻辑
   m.mu.Lock()
   read, _ = m.read.Load().(readOnly)
   // 如果加锁后发现read中有了
   if e, ok := read.m[key]; ok {
      // 如果e是被删除状态,将其更新为nil
      if e.unexpungeLocked() {
         // 并且给dirty中增加该键值对,因为此时dirty中没有
         m.dirty[key] = e
      }
      // 更新value
      e.storeLocked(&value)
   // read没有,但dirty有,更新dirty中该entry的值   
   } else  if e, ok := m.dirty[key]; ok {
      e.storeLocked(&value)
   // dirty,read都没有   
   } else {
      // 如果刚刚把dirty提升到read
      if !read.amended {
         // 将read浅拷贝到dirty中
         m.dirtyLocked()
         // 修改read.amended为true
         m.read.Store(readOnly{m: read.m, amended: true})
      }
      // 只将键值对加到dirty中
      m.dirty[key] = newEntry(value)
   }
   m.mu.Unlock()
}

Store整体流程为:

  • 如果read中存在该键值对,CAS更新其value

  • 若不存在,加锁,执行后面的逻辑:

    • 如果加锁后发现read中有了,该e是被删除状态,将其更新为nil,并且给dirty中增加该键值对,因为此时dirty中没有。然后更新e的值

    • 如果read没有,但dirty有,更新dirty中该entry的值,返回

    • 如果dirty,read都没有

      • 如果是刚提升dirty到read,此时dirty为空,需要将read浅拷贝到dirty中
      • 如果不是,则只在dirty中增加键值对

总的来说就是分各种情况处理:

  • read有:无锁更新read中的数据
  • read没有但dirty有:更新dirty中该entry的值
  • read没有dirty也没有:将新的键值对添加到dirty中

来看一些小函数:

func (e *entry) tryStore(i *interface{}) bool {
	for {
		p := atomic.LoadPointer(&e.p)
		if p == expunged {
			return false
		}
		if atomic.CompareAndSwapPointer(&e.p, p, unsafe.Pointer(i)) {
			return true
		}
	}
}
  • tryStore:当entry.p不是expunged时,通过CAS的方式设置value
func (m *Map) dirtyLocked() {
   if m.dirty != nil {
      return
   }
   // 将read浅拷贝到dirty中
   read, _ := m.read.Load().(readOnly)
   m.dirty = make(map[any]*entry, len(read.m))
   for k, e := range read.m {
      if !e.tryExpungeLocked() {
         m.dirty[k] = e
      }
   }
}
  • dirtyLocked:

    • 刚刚将dirty提升为read后,dirty为空,因此需要欧诺个read中浅拷贝一份。
    • 将read浅拷贝到dirty中,如果read中entry为空,该键值对就不会被拷贝到dirty,并将该entry置为expunged

Delete

func (m *Map) Delete(key any) {
   m.LoadAndDelete(key)
}

func (m *Map) LoadAndDelete(key any) (value any, loaded bool) {
   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 {
         // 如果该key不在read中,在dirty中,调用map原生的删除方法删除
         e, ok = m.dirty[key]
         delete(m.dirty, key)
         // 更新misses值
         m.missLocked()
      }
      m.mu.Unlock()
   }
   // 如果该key存在于read中,执行e.delete删除
   if ok {
      return e.delete()
   }
   return nil, false
}
  • 其中e.delete方法如下:

    • 如果已经是被删除状态,直接返回
    • 否则将e.p更新为nil
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
      }
   }
}

删除流程比较简单,如果在read里,就将其entry置位nil,如果不在read,就加锁去dirty删

为啥read的删除不像dirty一样,调用内置delete函数删除?

  • 因为read是只读结构,不能对hash表的结构做修改,而只能做逻辑删除,即将entry.p设为nil

由于这里已经被删除,重建ditry时(从read浅拷贝),如果发现该key对应的entry已经被删除,即等于nil,就不把该键值对复制到dirty

  • 为啥不复制该键值对?

    • 如果复制过去,但后续没有再对这个被删除的键值对进行操作,就会浪费内存空间
  • read中该被删除的key,啥时候真正删除

    • 假设后续没有对该key进行操作,等后续misses达到阈值,将dirty提升为read时,就能真正的从sync.map中删除该键值对
  • 如果后续对该key进行操作咋办?

    • 回到Store流程里:

    • // 如果加锁后发现read中有了
         if e, ok := read.m[key]; ok {
            // 如果该e是被删除状态,将其更新为nil
            if e.unexpungeLocked() {
               // 并且给dirty中增加该键值对,因为此时dirty中没有
               m.dirty[key] = e
            }
            // 更新value
            e.storeLocked(&value)
      
    • 若发现read中该entry为expunge,说明此时dirty中没有该键值对,因此需要去dirty中进行添加,同时将这次Store的新value放入entry中

    • 这也是sync.map设置expunge这个特殊值的意义所在:

      • 区分这个entry为空的键值对是否存在于dirty中,若为expunge,说明不在

Range

func (m *Map) Range(f func(key, value any) bool) {
   read, _ := m.read.Load().(readOnly)
   if read.amended {
      m.mu.Lock()
      read, _ = m.read.Load().(readOnly)
      if read.amended {
         read = readOnly{m: m.dirty}
         m.read.Store(read)
         m.dirty = nil
         m.misses = 0
      }
      m.mu.Unlock()
   }

   for k, e := range read.m {
      v, ok := e.load()
      if !ok {
         continue
      }
      if !f(k, v) {
         break
      }
   }
}

Range方法比较简单,如果dirty数据比read多,执行一次提升操作,然后遍历read

因为read不可变,所以这次遍历不会有并发安全问题,这也是copy on write思想的应用

总结

  • sync.Map 是线程安全的

  • 通过只读和可写分离,使得查询,更新已存在key的value不需要加锁

  • 随着程序的运行,dirty和read的差距会越来越大,使得需要加锁访问dirty的概率变大,效率也下降。因此当misses达到阈值时,将dirty提升为read,减低加锁的概率

  • 提升后第一次新增键值对时,会将read浅拷贝一份成为dirty,但会过滤掉entry为nil的键值对

  • 当 dirty 为 nil 的时候,read 就代表 map 所有的数据;当 dirty 不为 nil 的时候,dirty 才代表 map 所有的数据

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

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

相关文章

Golang 1.18 新特性模糊测试

一、Go 1.18新特性一览 0.官方博客&#xff1a;跳转 1.支持泛型 2.模糊测试 3.工作空间 4.性能提升 二、模糊测试介绍 模糊测试 &#xff08;fuzz testing, fuzzing&#xff09;是一种软件测试技术。其核心思想是將自动或半自动生成的随机数据输入到一个程序中&#xff0…

chatgpt赋能Python-python3怎么保存

Python 3如何高效地保存数据 Python是一门广泛使用的编程语言之一&#xff0c;其强大的工具和库使其成为从数据分析到机器学习等领域的首选语言。在处理大量数据时&#xff0c;数据的存储和访问变得非常关键。在这篇文章中&#xff0c;我们将讨论Python 3中如何保存数据以提高…

delphi6安装手册

DELPHI6安装手册 安装delphi6软件&#xff1a; 运行&#xff1a;\\dev2000 选择&#xff1a;set up→语言及开发工具→DELPHI6→双击图标install.exe 先后安装Delphi6和TeamSource 安装delphi6时会出现的要填序列号的情况 delphi6的安装序列号&#xff1a;在同一路径下双击ke…

chatgpt赋能Python-python3_9怎么调成黑色背景

Python是一种高级编程语言&#xff0c;它的版本不断发展和改进。最新发布的Python 3.9版本为用户提供了更多的新特性和改进&#xff0c;其中包括能够自定义代码编辑器背景颜色的新功能。本篇文章将介绍如何在Python 3.9中调整编辑器背景颜色为黑色&#xff0c;并探讨这个功能的…

三十九、分布式事务、seata、配置微服务客户端

1、事务 事务(TRANSACTION)是作为单个逻辑工作单元执行的一系列SQL操作&#xff0c;这些操作作为一个整体一起向系统提交&#xff0c;要么都执行、要么都不执行。 1.1 ACID事务的特点 原子性: 一致性&#xff1a;隔离性持久性 1.2 事务并发带来的问题 脏读 幻读 不可重复读 …

TCL字符串操作

format命令 因为 TCL 把所有的输入都当作字符串看待&#xff0c;所以 TCL 提供了较强的字符串操作功能&#xff0c;TCL 中与 字符串操作有关的命令有&#xff1a;string、format、regexp、regsub、scan 等。 语法&#xff1a;format formatstring ?vlue value...? format …

【Redis】电商项目秒杀问题之下单接口优化:Redis缓存、MQ以及lua脚本优化高并发背景下的秒杀下单问题

目录 一、优化思路 二、缓存库存与订单 1、库存缓存的redis数据结构 2、订单信息缓存的redis数据结构 三、整体流程 四、lua脚本确保权限校验操作的原子性 一、优化思路 【Redis】电商项目秒杀问题之超卖问题与一人一单问题_1373i的博客-CSDN博客https://blog.csdn.net/q…

chatgpt赋能Python-python3_打印

Python3 打印&#xff1a;一篇介绍性SEO文章 如果你是一名Python编程工程师&#xff0c;那么你一定知道在Python中打印是一项基本技能。在Python3中&#xff0c;打印已经发生了一些变化&#xff0c;本文将介绍Python3中打印的新特性和使用方法&#xff0c;并为您提供一些最佳实…

算法设计与分析:随机化算法(作业-必做)(头歌实验)

第1关&#xff1a;硬币实验 任务描述 本关任务&#xff1a;计算机产生的伪随机数来模拟抛硬币试验。 相关知识 为了完成本关任务&#xff0c;你需要掌握&#xff1a;1.如何获取数组的长度&#xff0c;2.如何遍历数组。 随机数 随机数在随机化算法设计中扮演着十分重要的角…

云原生CAx软件:gRPC

gRPC是Google基于HTTP/2协议开发的一套开源、跨平台的高性能RPC框架&#xff0c;可用于连接微服务架构内的各种服务&#xff0c;亦可以连接客户端与后端服务。 Ref. from gRPC gRPC is a modern open source high performance Remote Procedure Call (RPC) framework that can…

策划能力提升攻略:让你成为行业大咖

策划能力的提高是没有立竿见影的&#xff0c;首先你了解策划的本质吗?了解市场营销的本质吗?了解战略和策略的关系吗? 不是经常把什么IMC/USP/4P/4C/DNA/核心价值挂在嘴边&#xff0c;会做做波特SWOT分析、用用BCG的模型、MINKSY的7S模型这些就是策划了的。 别人的理论你可…

WEB AK赛

文章目录 web1_观字SSRF常见的URL绕过方式 web2_观星web3_观图web4_观心签到_观己 web1_观字 <?php#flag in http://192.168.7.68/flag if(isset($_GET[url])){$url $_GET[url];$protocol substr($url, 0,7);if($protocol!http://){die(仅限http协议访问);}if(preg_matc…

安装Maven 3.6.1:图文详细教程(适用于Windows系统)

一、官网下载对应版本 推荐使用maven3.6.1版本&#xff0c;对应下载链接&#xff1a; Maven3.6.1下载地址 或者&#xff0c;这里提供csdn下载地址&#xff0c;点击下载即可&#xff1a; Maven3.6.1直链下载 其他版本下载地址&#xff1a; 进入网址&#xff1a;http://mave…

【Linux之IO系统编程学习】01.open函数使用 代码实现touch命令效果

【Linux之IO系统编程学习】 项目代码获取&#xff1a;https://gitee.com/chenshao777/linux_-io.git &#xff08;麻烦点个免费的Star哦&#xff0c;您的Star就是我的写作动力&#xff01;&#xff09; 01.open函数使用 & 代码实现touch命令 一、open函数&#xff08;ma…

ARM处理器概论与组织

目录 1.ARM产品系列 2.体系结构 3.ARM指令集 定义&#xff1a; ARM的指令集&#xff1a; 4.编译原理 5.ARM存储模型 6.ARM的8种工作方式 ARM&#xff08;Advanced RISC Machines&#xff09;有三种含义 一个公司的名称、一类处理器的通称、一种技术&#xff0c;我们在这…

【中阳期货】人工智能AI与期货有什么 关系

人工智能&#xff08;AI&#xff09;和期货交易之间有许多相互影响的因素。AI可以帮助期货交易者在交易决策中更好地应对大量数据&#xff0c;加强交易系统预测能力&#xff0c;优化资产配置策略。以下是AI与期货交易的一些具体关系&#xff1a; 数据分析&#xff1a;AI有能力高…

python pickle反序列化分析

文章目录 前言Pickle的作用pickle反序列化pickletools和反序列化流程漏洞产生(__reduce__)R指令的绕过通过i和o指令触发 总结 前言 春秋杯中遇到了一道python题&#xff0c;使用的了numpy.loads()触发反序列化漏洞&#xff0c;百度学习了一下&#xff0c;发现numpy.load()会先…

【mysqlbinlog 恢复数据】

不小心把数据删掉了 首先要拿到binlog文件 命令行执行 /usr/local/mysql/bin/mysqlbinlog --base64-outputdecode-rows --start-datetime"2023-05-19 09:01:32" --stop-datetime"2023-05-19 09:01:35" -v /Users/zylong/Downloads/mysql-bin.003178 --re…

动态规划-状态机模型

大盗阿福 题目 链接&#xff1a;https://www.acwing.com/problem/content/1051/ 阿福是一名经验丰富的大盗。趁着月黑风高&#xff0c;阿福打算今晚洗劫一条街上的店铺。 这条街上一共有 N N N 家店铺&#xff0c;每家店中都有一些现金。 阿福事先调查得知&#xff0c;只…

chatgpt赋能Python-python3_9怎么下载

Python 3.9: 从哪里下载以及如何安装 Python是一种高级编程语言&#xff0c;被广泛使用于数据科学、人工智能、Web开发等领域。Python的最新版本是Python 3.9&#xff0c;它带来了一些新的特性和改进。对于那些希望尝试Python 3.9的人来说&#xff0c;了解如何下载和安装是很重…