继续浏览精彩内容
慕课网APP
程序员的梦工厂
打开
继续
感谢您的支持,我会继续努力的
赞赏金额会直接到老师账户
将二维码发送给自己后长按识别
微信支付
支付宝支付

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

幕布斯7119047
关注TA
已关注
手记 432
粉丝 28
获赞 99

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 的相关操作转成对应函数而已。

图片描述

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 是不能被复制的。

参考链接

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

作者:haohongfan
原文出处:https://www.cnblogs.com/457220157-FTD/p/14666852.html

打开App,阅读手记
0人推荐
发表评论
随时随地看视频慕课网APP