key-value 类型的数据结构,在项目中是非常常见的数据结构,属于基础的数据结构。
因为他每一个 key 都是唯一的索引值,所以能够通过索引很快的找到对应的值。
在 Go 语言中对应的数据结构就是 Map。
但是,Go 里面的 Map 数据结构,他并不是线程安全的,一但并发操作就会出现各种问题。
或许你现在项目中还没遇到,但是墨菲定律告诉我们,“凡是可能出错的事,必定会出错”;
所以,你可以将这篇文章加入收藏,踩坑了再来排坑。
Go 语言中 map 和 slice 不同,map 对象必须在使用之前初始化。
如果不初始化就直接赋值的话,就会出现 panic 异常,比如下面的代码:
var m map[int]int
m[1]=1
执行后会报错:
$ go run map.go
panic: assignment to entry in nil map
goroutine 1 [running]:
main.main()
/Users/kun/Desktop/课件/bingfa-demo/map.go:6 +0x27
exit status 2
非常好玩的是,不能赋值,但是却可以取值:
var m map[int]int
fmt.Println(m[1])
执行的结果是:
$ go run map.go
0
造成这个好玩的现象是因为:
从一个 nil 的 map 对象中获取值,并不会 panic,而是会得到一个零值。
所以切记 在使用 map 这个数据类型时,一定一定要初始化
,目前还没有工具可以检查是否初始化了,所以我们只能疯狂的记住它!
这是一个非常让人头大的问题,经常会出现本地调试OK,上线一出现并发就 panic,先来看一段代码:
var m = make(map[int]string,10) // 初始化一个map
go func() {
for {
m[1] = "hello" //设置key
}
}()
go func() {
for {
_ = m[2] //访问这个map
}
}()
select {}
解释下:
我初始化了一个定长 map 类型,然后我起了两个协程,一个去赋值,一个去取值。
看似没什么问题,但是你执行下,就会发现,他崩溃了!
$ go run map.go
fatal error: concurrent map read and map write
goroutine 18 [running]:
runtime.throw({0x10648e9, 0x0})
/usr/local/go/src/runtime/panic.go:1198 +0x71 fp=0xc00002af90 sp=0xc00002af60 pc=0x102b9d1
runtime.mapaccess1_fast64(0x0, 0x0, 0x2)
/usr/local/go/src/runtime/map_fast64.go:21 +0x172 fp=0xc00002afb0 sp=0xc00002af90 pc=0x100d672
main.main.func2()
/Users/kun/Desktop/课件/bingfa-demo/map.go:14 +0x2e fp=0xc00002afe0 sp=0xc00002afb0 pc=0x10556ce
runtime.goexit()
/usr/local/go/src/runtime/asm_amd64.s:1581 +0x1 fp=0xc00002afe8 sp=0xc00002afe0 pc=0x1052921
created by main.main
/Users/kun/Desktop/课件/bingfa-demo/map.go:12 +0x97
goroutine 1 [select (no cases)]:
main.main()
/Users/kun/Desktop/课件/bingfa-demo/map.go:17 +0x9c
goroutine 17 [runnable]:
main.main.func1()
/Users/kun/Desktop/课件/bingfa-demo/map.go:8 +0x35
created by main.main
/Users/kun/Desktop/课件/bingfa-demo/map.go:6 +0x65
exit status 2
因为 Go 内建的 map 对象并不是线程安全的,在并发读写时他会进行检查,就会出现错误!
但是庆幸的是,这个报错相对来说还是比较好定位的,通过错误提示基本都能快速找到报错点!
为了解决并发情况下 map 的操作,我们必须得自己去实现一个线程安全的结构体!
这里提供一个范例:
// RWMap 一个读写锁保护的线程安全的map
type RWMap struct {
sync.RWMutex // 读写锁保护下面的map字段
m map[int]int
}
// NewRWMap 新建一个RWMap
func NewRWMap(n int) *RWMap {
return &RWMap{
m: make(map[int]int, n),
}
}
// Get 获取值
func (m *RWMap) Get(k int) (int, bool) {
m.RLock()
defer m.RUnlock()
v, existed := m.m[k] // 在锁的保护下从map中读取
return v, existed
}
// Set 写值
func (m *RWMap) Set(k int, v int) {
m.Lock() // 锁保护
defer m.Unlock()
m.m[k] = v
}
// Delete 删除值
func (m *RWMap) Delete(k int) { //删除一个键
m.Lock() // 锁保护
defer m.Unlock()
delete(m.m, k)
}
// Len 获取长度
func (m *RWMap) Len() int { // map的长度
m.RLock() // 锁保护
defer m.RUnlock()
return len(m.m)
}
// Each 循环遍历
func (m *RWMap) Each(f func(k, v int) bool) { // 遍历map
m.RLock() //遍历期间一直持有读锁
defer m.RUnlock()
for k, v := range m.m {
if !f(k, v) {
return
}
}
}
因为 Go 目前为止并不支持泛型,所以我们并不能实现一个通用的加锁 map。
当然我们也可以通过 interface{} 来模拟泛型,但是成本太高,性能也会大打折扣。
我们一般在开发中,对 map 对象的操作无非就是 增删改查和遍历等几种操作。
我们在并发时,只需要对他进行加读写锁,就能防止 map 并发读写报的问题!
虽然读写锁可以提供一个线程安全的 map,但是在大量并发情况下,锁的竞争会非常激烈,于是也就有了 锁是性能下降的万恶之源 的说法。
于是我们在并发编程中的原则就是:尽量少的使用锁,如果要用,尽量做到减少锁的粒度和锁的持有时间。
基于这个思路,线程安全的 map 除了我们刚才使用的加读写锁的思路,还有分片加锁,sync.Map方案,感兴趣的可以去查阅相关的资料。
这里就不展开讲解了,因为一般我们对性能追求不是追求那么的极致,上读写锁就基本够用了。