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", []int1, 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", []int1, 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 作为存储介质,具体实现流程可阅读相关源码。

参考链接

以上是关于golang中的map并发读写问题: Golang 协程并发使用 Map 的正确姿势的主要内容,如果未能解决你的问题,请参考以下文章