map 不是并发安全的

官方的faq里有说明,考虑到有性能损失,map没有设计成原子操作,在并发读写时会有问题。

Map access is unsafe only when updates are occurring. As long as all goroutines are only reading—looking up elements in the map, including iterating through it using a for range loop—and not changing the map by assigning to elements or doing deletions, it is safe for them to access the map concurrently without synchronization.

查看源码,进一步立即实现机制

const (
  ...
    hashWriting  = 4 // a goroutine is writing to the map
    ...
)

type hmap struct {
    ...
    flags     uint8
    ...
}

map是检查是否有另外线程修改h.flag来判断,是否有并发问题。

// 在更新map的函数里检查并发写
    if h.flags&hashWriting == 0 {
        throw("concurrent map writes")
    }
    
// 在读map的函数里检查是否有并发写
    if h.flags&hashWriting != 0 {
        throw("concurrent map read and map write")
    }

测试并发问题的例子:一个goroutine不停地写,另一个goroutine不停地读

package main

import (
    "fmt"
    "time"
)

func main() {
    c := make(map[string]int)
    go func() { //开一个goroutine写map
        for j := 0; j < 1000000; j++ {
            c[fmt.Sprintf("%d", j)] = j
        }
    }()
    go func() { //开一个goroutine读map
        for j := 0; j < 1000000; j++ {
            fmt.Println(c[fmt.Sprintf("%d", j)])
        }
    }()
    time.Sleep(time.Second * 20)
}

立马产生错误

0
fatal error: concurrent map read and map write

goroutine 19 [running]:
runtime.throw(0x10d6ea4, 0x21)
        /usr/local/go/src/runtime/panic.go:774 +0x72 fp=0xc00009aef0 sp=0xc00009aec0 pc=0x10299c2
runtime.mapaccess1_faststr(0x10b41e0, 0xc000066180, 0x116ae71, 0x1, 0x1)
        /usr/local/go/src/runtime/map_faststr.go:21 +0x44f fp=0xc00009af60 sp=0xc00009aef0 pc=0x100ffff
main.main.func2(0xc000066180)

加sync.RWMutex来保护map

This statement declares a counter variable that is an anonymous struct containing a map and an embedded sync.RWMutex.

var counter = struct{
    sync.RWMutex
    m map[string]int
}{m: make(map[string]int)}
To read from the counter, take the read lock:

counter.RLock()
n := counter.m["some_key"]
counter.RUnlock()
fmt.Println("some_key:", n)
To write to the counter, take the write lock:

counter.Lock()
counter.m["some_key"]++
counter.Unlock()

针对上面有并发问题的测试例子,可以修改成以下代码:

package main

import (
    "fmt"
    "sync"
    "time"
)

func main() {
    var c = struct {
        sync.RWMutex
        m map[string]int
    }{m: make(map[string]int)}

    go func() { //开一个goroutine写map
        for j := 0; j < 1000000; j++ {
            c.Lock()
            c.m[fmt.Sprintf("%d", j)] = j
            c.Unlock()
        }
    }()
    go func() { //开一个goroutine读map
        for j := 0; j < 1000000; j++ {
            c.RLock()
            fmt.Println(c.m[fmt.Sprintf("%d", j)])
            c.RUnlock()
        }
    }()
    time.Sleep(time.Second * 20)
}
第三方 map 包

第三方包的实现都大同小异,基本上都是使用分离锁来实现并发安全的,具体分离锁来实现并发安全的原理可参考下面的延伸阅读

concurrent-map

m := cmap.New()
//写
m.Set("foo", "hello world")
m.Set("slice", []int{1, 2, 3, 4, 5, 6, 7})
m.Set("int", 1)
//读
m.Get("foo")  
m.Get("slice") 
m.Get("int")  
go-concurrentMap

m := concurrent.NewConcurrentMap()
m.Put("foo", "hello world")
m.Put("slice", []int{1, 2, 3, 4, 5, 6, 7})
m.Put("int", 1)
//读
m.Get("foo")  
m.Get("slice") 
m.Get("int") 
sync.Map

sync.Map 是官方出品的并发安全的 map,他在内部使用了大量的原子操作来存取键和值,并使用了 read 和 dirty 二个原生 map 作为存储介质,具体实现流程可阅读相关源码。

参考链接