go 语言中 map 默认是并发不安全的。
为什么要这么设计呢?
这就是矛与盾的关系,go 语言的设计者认为,在大部分场景中,对 map 的操作都非线程安全的;
我们不可能为了那小部分的需求,而牺牲大部分人的性能。
所以如果我们要使用线程安全的 map 的话,就需要做一些调整了。
那 go 语言中我们要使用线程安全的 map,该怎么操作呢?
首先我们先来看下,map 如果不注意线程安全会报什么错!
直接上一个代码:
package main
import (
"fmt"
"time"
)
func main() {
s := make(map[int]int)
// 启 100 个异步线程去写 map
for i := 0; i < 100; i++ {
go func(i int) {
s[i] = i
}(i)
}
// 启 100 个一步线程去读 map
for i := 0; i < 100; i++ {
go func(i int) {
fmt.Printf("map 第 %d 个元素值是 %d", i, s[i])
}(i)
}
// 睡眠 3 秒钟,方便前面的协程打印
time.Sleep(3 * time.Second)
}
这里我瞬间起了 100 个 协程去写值,然后又瞬间起 100 个协程去读里面的值。
因为是异步协程,所以 map 同一时刻就可能被多个写的协程操作。
那么运行后就会报错:fatal error: concurrent map writes
要解决线程安全问题,第一个方案就是给他家读写锁。
读取的时候加读锁,写的时候加写锁。
当然如果你想加互斥锁也是可以的,只要上锁就好了。
这里提供一个 demo 代码:
package main
import (
"fmt"
"sync"
"time"
)
func main() {
var lock sync.RWMutex
s := make(map[int]int)
for i := 0; i < 100; i++ {
go func(i int) {
lock.Lock()
s[i] = i
lock.Unlock()
}(i)
}
for i := 0; i < 100; i++ {
go func(i int) {
lock.RLock()
fmt.Printf("map 第 %d 个元素值是 %d", i, s[i])
lock.RUnlock()
}(i)
}
// 睡眠 3 秒钟,方便前面的协程打印
time.Sleep(3 * time.Second)
}
go 语言官方其实也为我们提供了一个线程安全的 map,就在 sync 包里面。
用他里面的 map 也是线程安全的。
这个 map 使用起来,就没有基础的 map 方便了,写值的时候得通过 Store 方法,读的时候使用方法 Load 来读取。
当然还有其他API,我放一个截图:
下面我也提供一个使用 demo 代码:
package main
import (
"fmt"
"sync"
"time"
)
func main() {
var m sync.Map
for i := 0; i < 100; i++ {
go func(i int) {
m.Store(i, i)
}(i)
}
for i := 0; i < 100; i++ {
go func(i int) {
v, ok := m.Load(i)
fmt.Printf("Load: %v, %v", v, ok)
}(i)
}
// 睡眠 3 秒钟,方便前面的协程打印
time.Sleep(3 * time.Second)
}
我们先来看下 sync.Map 的结构体:
type Map struct {
mu Mutex
read atomic.Value // readOnly
dirty map[interface{}]*entry
misses int
}
Go 语言的 sync.Map
支持并发读写,采取了 “空间换时间” 的机制,冗余了两个数据结构,分别是:read 和 dirty。
和我们的第一种方案 map+RWMutex 的实现并发的方式相比,减少了加锁对性能的影响。
它做了一些优化:可以无锁访问read map,而且会优先操作read map,倘若只操作read map就可以满足要求,那就不用去操作write map(dirty)。
所以在某些特定场景中它发生锁竞争的频率会远远小于 map+RWMutex 的实现方式
这样做的优点是:适合读多写少的场景;
缺点也就是:如果是写多的场景,会导致 read map 缓存失效,需要加锁,冲突变多,性能急剧下降。
你学废了么?