map是并发安全的吗?
首先我们写一段程序验证一下,创建两个goroutine,同时对一个map进行写操作,看看会发生什么吧!
func main() {
m := make(map[string]int)
m["foo"] = 1
var wg sync.WaitGroup
wg.Add(2)
go func() {
for i := 0; i < 1000; i++ {
m["foo"]++
}
wg.Done()
}()
go func() {
for i := 0; i < 1000; i++ {
m["foo"]++
}
wg.Done()
}()
wg.Wait()
}
欸出错了!
fatal error: concurrent map writes # 对map的并发写操作
因为它没有内置的锁机制来保护多个 goroutine 同时对其进行读写操作。当多个 goroutine 同时对同一个 map 进行读写操作时,就会出现数据竞争和不一致的结果。
就像上例那样,当两个 goroutine 同时尝试更新同一个键值对时,最终的结果可能取决于哪个 goroutine 先完成了更新操作。这种不确定性可能会导致程序出现错误或崩溃。
Go 语言团队没有将 map 设计成并发安全的,是因为这样会增加程序的开销并降低性能。
如果 map 内置了锁机制,那么每次访问 map 时都需要进行加锁和解锁操作,这会增加程序的运行时间并降低性能。
此外,并不是所有的程序都需要在并发场景下使用 map,因此将锁机制内置到 map 中会对那些不需要并发安全的程序造成不必要的开销。
在实际使用过程中,开发人员可以根据程序的需求来选择是否需要保证 map 的并发安全性,从而在性能和安全性之间做出权衡。
那怎么实现对map的安全的并发操作呢?
有三种方法,读写锁
,分片加锁
,sync.Map
-
读写锁,
type SafeMap struct { sync.RWMutex Map map[string]string } func NewSafeMap() *SafeMap { sm := new(SafeMap) sm.Map = make(map[string]string) return sm } func (sm *SafeMap) ReadMap(key string) string { sm.RLock() value := sm.Map[key] sm.RUnlock() return value } func (sm *SafeMap) WriteMap(key string, value string) { sm.Lock() sm.Map[key] = value sm.Unlock() } func main() { safeMap := NewSafeMap() var wg sync.WaitGroup // 启动多个goroutine进行写操作 for i := 0; i < 10; i++ { wg.Add(1) go func(i int) { defer wg.Done() safeMap.WriteMap(fmt.Sprintf("name%d", i), fmt.Sprintf("John%d", i)) }(i) } // 启动多个goroutine进行读操作 for i := 0; i < 10; i++ { wg.Add(1) go func(i int) { defer wg.Done() fmt.Println(safeMap.ReadMap(fmt.Sprintf("name%d", i))) }(i) } wg.Wait() for key, value := range safeMap.Map { fmt.Printf("Key: %s, Value: %s\n", key, value) } }
我们做了一个安全的map,在结构体内嵌入了sync.RWMutex,
goroutine进行读操作的时候先加读锁,读锁不会阻塞其他goroutine,
goroutine进行写操作的时候先加写锁,写锁会阻塞其他所有goroutine,
这样我们就实现了一个安全的map。
-
分片加锁,我们仿照java中的concurrentHashmap1.7之前的设计思路,对整个map的分片加锁,比如,一个map有十个槽位,我们对前五个和后五个槽位分别加锁,前后五个槽位的访问互不影响,这样就实现了安全的并发。
package main import ( "fmt" "sync" ) // N 我们将整个map分成16个分片 const N = 16 type SafeMap struct { //每个分片实际上就是一个小Map maps [N]map[string]string locks [N]sync.RWMutex } func NewSafeMap() *SafeMap { sm := new(SafeMap) for i := 0; i < N; i++ { sm.maps[i] = make(map[string]string) } return sm } func (sm *SafeMap) ReadMap(key string) string { //只对这个分片加锁 index := hash(key) % N sm.locks[index].RLock() value := sm.maps[index][key] sm.locks[index].RUnlock() return value } func (sm *SafeMap) WriteMap(key string, value string) { index := hash(key) % N sm.locks[index].Lock() sm.maps[index][key] = value sm.locks[index].Unlock() } func hash(s string) int { h := 0 for i := 0; i < len(s); i++ { h = 31*h + int(s[i]) } return h } func main() { safeMap := NewSafeMap() var wg sync.WaitGroup // 启动多个goroutine进行写操作 for i := 0; i < 10; i++ { wg.Add(1) go func(i int) { defer wg.Done() safeMap.WriteMap(fmt.Sprintf("name%d", i), fmt.Sprintf("John%d", i)) }(i) } // 启动多个goroutine进行读操作 for i := 0; i < 10; i++ { wg.Add(1) go func(i int) { defer wg.Done() fmt.Println(safeMap.ReadMap(fmt.Sprintf("name%d", i))) }(i) } wg.Wait() }
-
除了上面的两个方法之外,还可以使用go语言官方的map,sync.map
func main() { var m sync.Map var wg sync.WaitGroup // 启动多个goroutine进行写操作 for i := 0; i < 10; i++ { wg.Add(1) go func(i int) { defer wg.Done() // 写操作,store m.Store(fmt.Sprintf("name%d", i), fmt.Sprintf("John%d", i)) }(i) } // 启动多个goroutine进行读操作 for i := 0; i < 10; i++ { wg.Add(1) go func(i int) { defer wg.Done() // 读操作load v, _ := m.Load(fmt.Sprintf("name%d", i)) fmt.Println(v.(string)) }(i) } wg.Wait() }