• Issue: 为什么 map 并发不安全,读也不安全吗?

代码测试

单个 goroutine 读写 map 的 Demo:

结论:正常

多个 goroutine 只写 的 Demo:

 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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
package main

import "time"

var cache map[string]string

func init() {
	cache = make(map[string]string)
}

func main() {
	go func() {
		cache["a"] = "a"
	}()

	go func() {
		cache["b"] = "b"
	}()

	go func() {
		cache["c"] = "c"
	}()

	time.Sleep(1 * time.Second)
}

// $ go run -race main.go
// ==================                                              
// WARNING: DATA RACE                                              
// Write at 0x00c042055d10 by goroutine 6:                         
//   runtime.mapassign_faststr()                                   
//       D:/Go/src/runtime/hashmap_fast.go:694 +0x0                
//   main.main.func2()                                             
//       D:/code/Go_Path/src/instance.golang.com/main.go:17 +0x76  
                                                                
// Previous write at 0x00c042055d10 by goroutine 5:                
//   runtime.mapassign_faststr()                                   
//       D:/Go/src/runtime/hashmap_fast.go:694 +0x0                
//   main.main.func1()                                             
//       D:/code/Go_Path/src/instance.golang.com/main.go:13 +0x76  
                                                                
// Goroutine 6 (running) created at:                               
//   main.main()                                                   
//       D:/code/Go_Path/src/instance.golang.com/main.go:16 +0x65  
                                                                
// Goroutine 5 (finished) created at:                              
//   main.main()                                                   
//       D:/code/Go_Path/src/instance.golang.com/main.go:12 +0x4d  
// ==================                                              
// Found 1 data race(s)                                            
// exit status 66                                  

结论:Write data race

多个 goroutine 读写 的 Demo:

 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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
package main

import "time"

var cache map[string]string

func init() {
	cache = make(map[string]string)
}

func main() {
	go func() {
		cache["a"] = "a"
	}()

	go func() {
		cache["b"] = "b"
	}()

	go func() {
		println(cache["a"])
	}()

	time.Sleep(1 * time.Second)
}

// $ go run -race main.go
// ==================
// WARNING: DATA RACE
// Write at 0x00c042055d10 by goroutine 6:
//   runtime.mapassign_faststr()
//       D:/Go/src/runtime/hashmap_fast.go:694 +0x0
//   main.main.func2()
//       D:/code/Go_Path/src/instance.golang.com/main.go:17 +0x76

// Previous write at 0x00c042055d10 by goroutine 5:
//   runtime.mapassign_faststr()
//       D:/Go/src/runtime/hashmap_fast.go:694 +0x0
//   main.main.func1()
//       D:/code/Go_Path/src/instance.golang.com/main.go:13 +0x76

// Goroutine 6 (running) created at:
//   main.main()
//       D:/code/Go_Path/src/instance.golang.com/main.go:16 +0x65

// Goroutine 5 (finished) created at:
//   main.main()
//       D:/code/Go_Path/src/instance.golang.com/main.go:12 +0x4d
// ==================
// ==================
// WARNING: DATA RACE
// Read at 0x00c04207c088 by goroutine 7:
//   main.main.func3()
//       D:/code/Go_Path/src/instance.golang.com/main.go:21 +0x89

// Previous write at 0x00c04207c088 by goroutine 5:
//   main.main.func1()
//       D:/code/Go_Path/src/instance.golang.com/main.go:13 +0x89

// Goroutine 7 (running) created at:
//   main.main()
//       D:/code/Go_Path/src/instance.golang.com/main.go:20 +0x7d

// Goroutine 5 (finished) created at:
//   main.main()
//       D:/code/Go_Path/src/instance.golang.com/main.go:12 +0x4d
// ==================
// a
// Found 2 data race(s)
// exit status 66

结论:Write & Read data race

多个 goroutine 只读 的 Demo:

 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
31
package main

import "time"

var cache map[string]string

func init() {
	cache = make(map[string]string)
	cache["a"] = "a"
}

func main() {
	go func() {
		println(cache["a"])
	}()

	go func() {
		println(cache["a"])
	}()

	go func() {
		println(cache["a"])
	}()

	time.Sleep(1 * time.Second)
}

// $ go run -race main.go
// a
// a
// a

结论:Read 没有 data race

总结

Golang 中 map 只有在并发 的时候才是线程安全的,其他情况:并发 或 并发 读 & 写 都属于并发不安全。

那么,什么是并发不安全?

并发不安全指的是:多个并发的线程同时访问一个变量的时候,如果都有修改,没法确定哪个修改最终生效。比如 A 和 B 并发,同时对变量 i 做 +1 操作,假设 i 初始值为 0,那么最终结果可能是1,也可能是2,这就叫并发不安全。

并发不安全基本上不会导致程序报错,只是执行的结果会和期望的不一样,不可控

map 并发访问不安全的原因?

因为 map 是引用类型,所以即使是函数传值调用,参数的副本依然指向原始的 map, 所以多个 goroutine 并发访问的实际上是同一个 map,即:对共享变量、资源的并发访问,会产生资源竞争,故资源共享遭到破坏。

 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
31
32
33
34
35
36
37
38
39
40
41
package main

import "fmt"

func main() {
	cache := make(map[int]int)
	fmt.Printf("map: %p\n", cache)

	key := 1
	fmt.Printf("key: %p\n", &key)
	fmt.Println()

	writeMap(cache, key, 1)
	readMap(cache, key)
}

func writeMap(m map[int]int, key, value int) {
	fmt.Printf("map: %p\n", m)
	fmt.Printf("key: %p\n", &key)
	fmt.Println()
	m[key] = value
}

func readMap(m map[int]int, key int) int {
	fmt.Printf("map: %p\n", m)
	fmt.Printf("key: %p\n", &key)
	fmt.Println()
	return m[key]
}

// $ go run main.go
// map: 0xc04206e1e0
// key: 0xc04205a088

// map: 0xc04206e1e0
// key: 0xc04205a0a0

// map: 0xc04206e1e0
// key: 0xc04205a0a8
//
// Note: map 的地址始终指向同一块内存空间

优化方案

官方文档:

Go maps in action: Concurrency 说明:

Maps are not safe for concurrent use: it’s not defined what happens when you read and write to them simultaneously. If you need to read from and write to a map from concurrently executing goroutines, the accesses must be mediated(调解) by some kind of synchronization mechanism. One common way to protect maps is with sync.RWMutex.

Go FAQ 解释如下:

After long discussion it was decided that the typical use of maps did not require safe access from multiple goroutines, and in those cases where it did, the map was probably part of some larger data structure or computation that was already synchronized. Therefore requiring that all map operations grab a mutex would slow down most programs and add safety to few. This was not an easy decision, however, since it means uncontrolled map access can crash the program.

大致意思就是说,并发访问map是不安全的,会出现未定义行为,导致程序退出。所以如果希望在多协程中并发访问map,必须提供某种同步机制,一般情况下通过 读写锁sync.RWMutex 实现对map的并发访问控制,将map和sync.RWMutex封装一下,可以实现对map的安全并发访问,示例代码参考这里 Here

1
2
3
4
type cache struct {
    rw      sync.RWMutex
    items   map[string]interface{}
}