0%

golang中的map并发读写问题

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.

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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不停地读

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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)
}

立马产生错误

1
2
3
4
5
6
7
8
9
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.

1
2
3
4
var counter = struct{
sync.RWMutex
m map[string]int
}{m: make(map[string]int)}

To read from the counter, take the read lock:

1
2
3
4
counter.RLock()
n := counter.m["some_key"]
counter.RUnlock()
fmt.Println("some_key:", n)

To write to the counter, take the write lock:

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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)
}

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?