[Go] sync.Map

229 阅读2分钟

问题描述

Go语言中的 map 在并发情况下,只读是线程安全的,同时读写是线程不安全的。

错误信息显示,并发的 map 读和 map 写,也就是说使用了两个并发函数不断地对 map 进行读和写而发生了竞态问题,map 内部会对这种并发操作进行检查并提前发现。

需要并发读写时,一般的做法是加锁,但这样性能并不高,Go语言在 1.9 版本中提供了一种效率较高的并发安全的 sync.Map,sync.Map 和 map 不同,不是以语言原生形态提供,而是在 sync 包下的特殊结构。

问题演示

下面来看下并发情况下读写 map 时会出现的问题,代码如下:

func main() {
	// 创建一个int到int的映射
	m := make(map[int]int)

	// 开启一段并发代码
	go func() {
		// 不停地对map进行写入
		for {
			m[1] = 1
		}
	}()
	
	// 开启一段并发代码
	go func() {
		// 不停地对map进行读取
		for {
			_ = m[1]
		}
	}()
	// 无限循环, 让并发程序在后台执行
	for {
	}
}

报错信息:

fatal error: concurrent map read and map write

sync.Map

sync.Map 有以下特性:

  • 无须初始化,直接声明即可。
  • sync.Map 不能使用 map 的方式进行取值和设置等操作,而是使用 sync.Map 的方法进行调用,Store 表示存储,Load 表示获取,Delete 表示删除。
  • 使用 Range 配合一个回调函数进行遍历操作,通过回调函数返回内部遍历出来的值,Range 参数中回调函数的返回值在需要继续迭代遍历时,返回 true,终止迭代遍历时,返回 false。

基于 sync.Map 改写以上代码,可以得到如下的代码:

func main() {
	// 创建一个int到int的映射
	m := sync.Map{}

	// 开启一段并发代码
	go func() {
		// 不停地对map进行写入
		for {
			m.Store(1,1)
		}
	}()

	// 开启一段并发代码
	go func() {
		// 不停地对map进行读取
		for {
			_, _ = m.Load(1)
		}
	}()
	// 无限循环, 让并发程序在后台执行
	for {
	}
}

sync.Map 没有提供获取 map 数量的方法,替代方法是在获取 sync.Map 时遍历自行计算数量,sync.Map 为了保证并发安全有一些性能损失,因此在非并发情况下,使用 map 相比使用 sync.Map 会有更好的性能。