golang中的map并发读写问题: Golang 协程并发使用 Map 的正确姿势

2022-09-28 09:11:02 浏览数 (1)

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.

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

代码语言:javascript复制
const (
  ...
    hashWriting  = 4 // a goroutine is writing to the map
    ...
)

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

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

代码语言:javascript复制
// 在更新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不停地读

代码语言:javascript复制
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)
}

立马产生错误

代码语言:javascript复制
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.

代码语言:javascript复制
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()

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

代码语言:javascript复制
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

代码语言:javascript复制
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 作为存储介质,具体实现流程可阅读相关源码。

参考: https://learnku.com/articles/27691

参考链接

  1. The Go Blog - Go maps in action
  2. Why are map operations not defined to be atomic?

0 人点赞