深度解密 Go 语言中的 sync.map

2020-06-16 17:00:00 来源:易采站长站 作者:于海丽

工作中,经常会碰到并发读写 map 而造成 panic 的情况,为什么在并发读写的时候,会 panic 呢?因为在并发读写的情况下,map 里的数据会被写乱,之后就是 Garbage in, garbage out,还不如直接 panic 了。

是什么

Go 语言原生 map 并不是线程安全的,对它进行并发读写操作的时候,需要加锁。而 sync.map 则是一种并发安全的 map,在 Go 1.9 引入。

sync.map 是线程安全的,读取,插入,删除也都保持着常数级的时间复杂度。
sync.map 的零值是有效的,并且零值是一个空的 map。在第一次使用之后,不允许被拷贝。

有什么用

一般情况下解决并发读写 map 的思路是加一把大锁,或者把一个 map 分成若干个小 map,对 key 进行哈希,只操作相应的小 map。前者锁的粒度比较大,影响效率;后者实现起来比较复杂,容易出错。

而使用 sync.map 之后,对 map 的读写,不需要加锁。并且它通过空间换时间的方式,使用 read 和 dirty 两个 map 来进行读写分离,降低锁时间来提高效率。

如何使用

使用非常简单,和普通 map 相比,仅遍历的方式略有区别:

package main

import (
	"fmt"
	"sync"
)

func main() {
	var m sync.Map
	// 1. 写入
	m.Store("qcrao", 18)
	m.Store("stefno", 20)

	// 2. 读取
	age, _ := m.Load("qcrao")
	fmt.Println(age.(int))

	// 3. 遍历
	m.Range(func(key, value interface{}) bool {
		name := key.(string)
		age := value.(int)
		fmt.Println(name, age)
		return true
	})

	// 4. 删除
	m.Delete("qcrao")
	age, ok := m.Load("qcrao")
	fmt.Println(age, ok)

	// 5. 读取或写入
	m.LoadOrStore("stefno", 100)
	age, _ = m.Load("stefno")
	fmt.Println(age)
}

第 1 步,写入两个 k-v 对;

第 2 步,使用 Load 方法读取其中的一个 key;

第 3 步,遍历所有的 k-v 对,并打印出来;

第 4 步,删除其中的一个 key,再读这个 key,得到的就是 nil;

第 5 步,使用 LoadOrStore,尝试读取或写入 "Stefno",因为这个 key 已经存在,因此写入不成功,并且读出原值。

程序输出:

18
stefno 20
qcrao 18
<nil> false
20

sync.map 适用于读多写少的场景。对于写多的场景,会导致 read map 缓存失效,需要加锁,导致冲突变多;而且由于未命中 read map 次数过多,导致 dirty map 提升为 read map,这是一个 O(N) 的操作,会进一步降低性能。

源码分析

数据结构

先来看下 map 的数据结构。去掉大段的注释后:

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

互斥量 mu 保护 read 和 dirty。

微信扫一扫

易采站长站微信账号