hi, 大家好,我是 haohongfan。

本篇文章会从使用方式和原码角度剖析 sync.Map。不过不管是日常开发还是开源项目中,好像 sync.Map 并没有得到很好的利用,大家还是习惯使用 Mutex + Map 来使用。

下面这段代码,看起来很有道理,其实是用错了(背景:并发场景中获取注册信息)。

instance, ok := instanceMap[name]
if ok {
return instance, nil
} initLock.Lock()
defer initLock.Unlock() // double check
instance, ok = instanceMap[name]
if ok {
return instance, nil
}

这里使用使用 sync.Map 会更合理些,因为 sync.Map 底层完全包含了这个逻辑。可能写 Java 的同学看着上面这段代码很眼熟,但确实是用错了,关于为什么用错了以及会造成什么影响,请大家关注后续的文章。

我大概分析了下大家宁愿使用 Mutex + Map,也不愿使用 sync.Map 的原因:

  1. sync.Map 本身就很难用,使用起来并不像一个 Map。失去了 map 应有的特权语法,如:make, map[1] 等
  2. sync.Map 方法较多。让一个简单的 Map 使用起来有了较高的学习成本。

不管什么样的原因吧,当你读过这篇文章后,在某些特定的并发场景下,建议使用 sync.Map 代替 Map + Mutex 的。

用法全解

package main

import (
"fmt"
"sync"
) func main() {
var syncMap sync.Map
syncMap.Store("11", 11)
syncMap.Store("22", 22) fmt.Println(syncMap.Load("11")) // 11
fmt.Println(syncMap.Load("33")) // 空 fmt.Println(syncMap.LoadOrStore("33", 33)) // 33
fmt.Println(syncMap.Load("33")) // 33
fmt.Println(syncMap.LoadAndDelete("33")) // 33
fmt.Println(syncMap.Load("33")) // 空 syncMap.Range(func(key, value interface{}) bool {
fmt.Printf("key:%v value:%v\n", key, value)
return true
})
// key:22 value:22
// key:11 value:11
}

其实 sync.Map 并不复杂,只是将普通 map 的相关操作转成对应函数而已。

普通 map sync.Map
map 获取某个 key map[1] sync.Load(1)
map 添加元素 map[1] = 10 sync.Store(1, 10)
map 删除一个 key delete(map, 1) sync.Delete(1)
遍历 map for...range sync.Range()

sync.Map 两个特有的函数,不过从字面就能理解是什么意思了。

LoadOrStore:sync.Map 存在就返回,不存在就插入

LoadAndDelete:sync.Map 获取某个 key,如果存在的话,同时删除这个 key

源码解析

type Map struct {
mu Mutex
read atomic.Value // readOnly read map
dirty map[interface{}]*entry // dirty map
misses int
}

read map 的值是什么时间更新的 ?

  1. Load/LoadOrStore/LoadAndDelete 时,当 misses 数量大于等于 dirty map 的元素个数时,会整体复制 dirty map 到 read map
  2. Store/LoadOrStore 时,当 read map 中存在这个key,则更新
  3. Delete/LoadAndDelete 时,如果 read map 中存在这个key,则设置这个值为 nil

dirty map 的值是什么时间更新的 ?

  1. 完全是一个新 key, 第一次插入 sync.Map,必先插入 dirty map
  2. Store/LoadOrStore 时,当 read map 中不存在这个key,在 dirty map 存在这个key,则更新
  3. Delete/LoadAndDelete 时,如果 read map 中不存在这个key,在 dirty map 存在这个key,则从 dirty map 中删除这个key
  4. 当 misses 数量大于等于 dirty map 的元素个数时,会整体复制 dirty map 到 read map,同时设置 dirty map 为 nil

疑问:当 dirty map 复制到 read map 后,将 dirty map 设置为 nil,也就是 dirty map 中就不存在这个 key 了。如果又新插入某个 key,多次访问后达到了 dirty map 往 read map 复制的条件,如果直接用 read map 覆盖 dirty map,那岂不是就丢了之前在 read map 但不在 dirty map 的 key ?

答:其实并不会。当 dirty map 向 read map 复制后,readOnly.amended 等于了 false。当新插入了一个值时,会将 read map 中的值,重新给 dirty map 赋值一遍,也就是 read map 也会向 dirty map 中复制。

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
}
}
}

read map 和 dirty map 是什么时间删除的?

  • 当 read map 中存在某个 key 的时候,这个时候只会删除 read map, 并不会删除 dirty map(因为 dirty map 不存在这个值)
  • 当 read map 中不存在时,才会去删除 dirty map 里面的值

疑问:如果按照这个删除方式,那岂不是 dirty map 中会有残余的 key,导致没删除掉?

答:其实并不会。当 misses 数量大于等于 dirty map 的元素个数时,会整体复制 dirty map 到 read map。这个过程中还附带了另外一个操作:将 dirty map 置为 nil。

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
}

read map 与 dirty map 的关系 ?

  1. 在 read map 中存在的值,在 dirty map 中可能不存在。
  2. 在 dirty map 中存在的值,在 read map 中也可能存在。
  3. 当访问多次,发现 dirty map 中存在,read map 中不存在,导致 misses 数量大于等于 dirty map 的元素个数时,会整体复制 dirty map 到 read map。
  4. 当出现 dirty map 向 read map 复制后,dirty map 会被置成 nil。
  5. 当出现 dirty map 向 read map 复制后,readOnly.amended 等于了 false。当新插入了一个值时,会将 read map 中的值,重新给 dirty map 赋值一遍

read/dirty map 中的值一定是有效的吗?

并不一定。放入到 read/dirty map 中的值总共有 3 种类型:

  • nil:如果获取到的 value 是 nil,那说明这个 key 是已经删除过的。既不在 read map,也不在 dirty map
  • expunged:这个 key 在 dirty map 中是不存在的
  • valid:其实就正常的情况,要么这个值存在在 read map 中,要么存在在 dirty map 中

sync.Map 是如何提高性能的?

通过源码解析,我们知道 sync.Map 里面有两个普通 map,read map主要是负责读,dirty map 是负责读和写(加锁)。在读多写少的场景下,read map 的值基本不发生变化,可以让 read map 做到无锁操作,就减少了使用 Mutex + Map 必须的加锁/解锁环节,因此也就提高了性能。

不过也能够看出来,read map 也是会发生变化的,如果某些 key 写操作特别频繁的话,sync.Map 基本也就退化成了 Mutex + Map(有可能性能还不如 Mutex + Map)。

所以,不是说使用了 sync.Map 就一定能提高程序性能,我们日常使用中尽量注意拆分粒度来使用 sync.Map。

关于如何分析 sync.Map 是否优化了程序性能,同样可以使用 pprof。具体过程可以参考 《这可能是最容易理解的 Go Mutex 源码剖析》

sync.Map 应用场景

  1. 读多写少
  2. 写操作也多,但是修改的 key 和读取的 key 特别不重合。

关于第二点我觉得挺扯的,毕竟我们很难把控这一点,不过由于是官方的注释还是放在这里。

实际开发中我们要注意使用场景和擅用 pprof 来分析程序性能。

sync.Map 使用注意点

和 Mutex 一样, sync.Map 也同样不能被复制,因为 atomic.Value 是不能被复制的。

参考链接

  1. https://golang.design/under-the-hood/zh-cn/part1basic/ch05sync/map/
  2. https://draveness.me/golang-sync-primitives/
  3. https://github.com/golang/go/blob/master/src/sync/map.go

sync.Map 完整流程图获取链接:链接: https://pan.baidu.com/s/16yEnZFbXwSe3qkvX1zi-Wg 密码: 8w8k。

看过这篇剖析,你还不懂 Go sync.Map 吗?的更多相关文章

  1. 【最短路径Floyd算法详解推导过程】看完这篇,你还能不懂Floyd算法?还不会?

    简介 Floyd-Warshall算法(Floyd-Warshall algorithm),是一种利用动态规划的思想寻找给定的加权图中多源点之间最短路径的算法,与Dijkstra算法类似.该算法名称以 ...

  2. [转帖]看完这篇文章你还敢说你懂JVM吗?

    看完这篇文章你还敢说你懂JVM吗? 在一些物理内存为8g的服务器上,主要运行一个Java服务,系统内存分配如下:Java服务的JVM堆大小设置为6g,一个监控进程占用大约 600m,Linux自身使用 ...

  3. 看完此文还不懂NB-IoT,你就过来掐死我吧...【转】

    转自:https://www.cnblogs.com/pangguoming/p/9755916.html 看完此文还不懂NB-IoT,你就过来掐死我吧....... 1 1G-2G-3G-4G-5G ...

  4. 看完此文还不懂NB-IoT,你就过来掐死我吧...

    看完此文还不懂NB-IoT,你就过来掐死我吧....... 1 1G-2G-3G-4G-5G 不解释,看图,看看NB-IoT在哪里? 2 NB-IoT标准化历程 3GPP NB-IoT的标准化始于20 ...

  5. 看完这篇还不会 GestureDetector 手势检测,我跪搓衣板!

    引言 在 android 开发过程中,我们经常需要对一些手势,如:单击.双击.长按.滑动.缩放等,进行监测.这时也就引出了手势监测的概念,所谓的手势监测,说白了就是对于 GestureDetector ...

  6. 看完这篇还不清楚Netty的内存管理,那我就哭了!

    说明 在学习Netty的时候,ByteBuf随处可见,但是如何高效分配ByteBuf还是很复杂的,Netty的池化内存分配这块还是比较难的,很多人学习过,看过但是还是云里雾里的,本篇文章就是主要来讲解 ...

  7. [转帖]看完这篇文章,我奶奶都懂了https的原理

    看完这篇文章,我奶奶都懂了https的原理 http://www.17coding.info/article/22 非对称算法 以及 CA证书 公钥 核心是 大的质数不一分解 还有 就是 椭圆曲线算法 ...

  8. 看完这篇Redis缓存三大问题,保你面试能造火箭,工作能拧螺丝。

    前言 日常的开发中,无不都是使用数据库来进行数据的存储,由于一般的系统任务中通常不会存在高并发的情况,所以这样看起来并没有什么问题. 一旦涉及大数据量的需求,如一些商品抢购的情景,或者主页访问量瞬间较 ...

  9. v77.01 鸿蒙内核源码分析(消息封装篇) | 剖析LiteIpc(上)进程通讯内容 | 新的一年祝大家生龙活虎 虎虎生威

    百篇博客分析|本篇为:(消息封装篇) | 剖析LiteIpc进程通讯内容 进程通讯相关篇为: v26.08 鸿蒙内核源码分析(自旋锁) | 当立贞节牌坊的好同志 v27.05 鸿蒙内核源码分析(互斥锁 ...

随机推荐

  1. Flutter 使用 flare

    video flare_flutter 工作示例 install dependencies: flare_flutter: ^1.5.5 assets: - assets/flr/switch_day ...

  2. 人物传记JULLIAN MURPHY:投资哪家强,区块链必然>股票+房地产

    今年上半年在金融股市出现巨大波动的时候,星盟的项目审核经理JULLIAN MURPHY发现了一个有趣的现象:各种熔断和暴跌的背后,特斯拉的股票却从去年年末开始至今已经暴涨了12倍,即便中途有所回落,但 ...

  3. PBN进场程序保护区图例分析

      疫情仍在持续,除了待家里不给祖国添乱之外,过去没有时间去完成的事情,现在可以静下心来认真面对,充实过好每一天. 今天想跟大家聊一下ICAO 8168第二卷PBN进场程序的图例. 就是下面这张图: ...

  4. .NET探索模型路由约定实现伪静态

    概述 IPageRouteModelConvention接口用于自定义PageRouteModel,这个对象在Microsoft.AspNetCore.Mvc.ApplicationModels命名空 ...

  5. Vue(1)

    一:概述 Vue是一套用于构建用户界面的渐进式JavaScript框架,与其它大型框架不同的是,Vue被设计为可以自底向上逐层应用.Vue的核心库只关心视图层,不仅易于上手,还便于与第三方库或既有项目 ...

  6. CentOS 7关闭firewalld启用iptables 开放端口

    在CentOS7中,有很多CentOS 6中的常用服务发生了变化. 其中iptables是其中比较大的一个.防火墙iptables被firewalld取代. 本文将介绍,如果采用systemctl关闭 ...

  7. js浅拷贝(地址引用)和深拷贝(克隆)

    浅拷贝和深拷贝相对于引用类型而言的. js有两大类型值类型(基本数据类型)和引用类型(object,function,array): 值类型保存在栈上,引用类型保存在堆上. 浅拷贝只是单纯的拷贝对象的 ...

  8. js和c#小数四舍五入

    <script language="javascript"> document.write("<h1>JS保留两位小数例子</h1>& ...

  9. 精确率precession和召回率recall

    假设有两类样本,A类和B类,我们要衡量分类器分类A的能力. 现在将所有样本输入分类器,分类器从中返回了一堆它认为属于A类的样本. 召回率:分类器认为属于A类的样本里,真正是A类的样本数,占样本集中所有 ...

  10. 初探JavaScript原型链污染

    18年p师傅在知识星球出了一些代码审计题目,其中就有一道难度为hard的js题目(Thejs)为原型链污染攻击,而当时我因为太忙了(其实是太菜了,流下了没技术的泪水)并没有认真看过,后续在p师傅写出w ...