看过这篇剖析,你还不懂 Go sync.Map 吗?

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
}
sync map 全景图
Load
Store
Delete

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/1RIX6NKj8UhWkdFyFHptWwg 密码: rsg9。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,458评论 6 513
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 94,030评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,879评论 0 358
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,278评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,296评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,019评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,633评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,541评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,068评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,181评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,318评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,991评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,670评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,183评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,302评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,655评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,327评论 2 358

推荐阅读更多精彩内容

  • 偶然看见这么篇文章:一道并发和锁的golang面试题。 虽然年代久远,但也稍有兴趣。 正好最近也看到了 sync....
    一块牛排阅读 1,864评论 0 1
  • map并发读线程安全,并发读写线程不安全。 sync.Map 读写分离 空间换时间 Map Golang1.6之前...
    JunChow520阅读 257评论 0 0
  • map map的底层实现 golang中的map采用了HashTable的实现,通过数组+链表实现的。一个哈希表会...
    xixisuli阅读 3,799评论 0 1
  • [TOC] 本文基于1.10源码分析如之前的文章可以看到,golang中的map是不支持并发操作的,golang推...
    123archu阅读 8,499评论 1 8
  • 版本 go version 1.10.1 使用方法 数据结构 mu: Mutex锁,在对dirty进行操作的时候,...
    不就是个名字么不要在意阅读 546评论 0 0