文章摘要
GPT 4
此内容根据文章生成,并经过人工审核,仅用于文章内容的解释与总结
投诉

go map并发读写问题

问题引入

go语言的map是不允许并发读写的,例如下面这段程序:

  • 并发写入
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func main() {
var m = make(map[int]int)
var wg sync.WaitGroup
wg.Add(2)
// 启动两个协程序同时写入 map
go func() {
for i := 0; i < 100000; i++ {
m[i] = i // 写入
}
wg.Done()
}()
go func() {
for i := 0; i < 100000; i++ {
m[i] = i // 写入
}
wg.Done()
}()
wg.Wait()
}

运行便会报错:fatal error: concurrent map writes

  • 并发读写
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func main() {
var m = make(map[int]int)
var wg sync.WaitGroup
wg.Add(2)
// 启动两个协程序同时写入 map
go func() {
for i := 0; i < 100000; i++ {
m[i] = i // 写入
}
wg.Done()
}()
go func() {
for i := 0; i < 100000; i++ {
fmt.Println(m[i]) // 读取
}
wg.Done()
}()
wg.Wait()
}

运行会报错:fatal error: concurrent map read and map write

为什么go语言的map允许并发读写呢?

这跟 map 的实现有关,根本原因是:map 的底层是支持自动扩容的,在添加元素的时候,如果发现容量不够,就会自动扩容。 如果允许扩容和访问操作同时发生,那么访问到的数据就不一定就是我们之前存放进去的了,所以 Go 从设计上就禁止了这种操作。 也就是 fail fast 的原则。

还有一种不会报错的map读写错误:

下面用一个有趣的例子去复现一个由于迭代过程中修改map而引发的问题:

1
2
3
4
5
6
7
8
9
10
11
12
func main() {
var m = make(map[int]int)

m[0] = 0

for i := range m {
m[i+100] = i + 100
}

fmt.Println(m)
fmt.Println(len(m))
}

在这个例子中,我们只给map赋了一个初值,然后在迭代过程中进行修改,让map进行扩充,多跑几次查看允许结果:

1
2
3
4
5
6
map[0:0 100:100 200:200 300:300]
4
map[0:0 100:100 200:200 300:300 400:400 500:500 600:600 700:700]
8
map[0:0 100:100 200:200 300:300 400:400 500:500]
6

我们发现每次不只是只迭代一次,并且每次迭代次数不同,这就是在迭代map的时候扩充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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
package main

import (
"sync"
)

// 获取整合的倒排索引字符串
func getIntegrateInvertedIndexString(n int) map[string]string {
indexStrings := make(map[string]string)

channel := make(chan map[string]string, n)

var wg sync.WaitGroup

// 合并部分结果
go func() {
for partIndex := range channel {
// 锁定访问共享的 map
for key, value := range partIndex {
if _, ok := indexStrings[key]; !ok {
indexStrings[key] = value
} else {
indexStrings[key] += "-" + value
}
}
}
}()

// 启动多个goroutine来计算部分结果
for i := 0; i < n; i++ {
wg.Add(1)
go func() {
defer wg.Done()
indexString := integrateInvertedIndexString()
if indexString != nil {
channel <- indexString
}
}()
}

wg.Wait()
close(channel)
return indexStrings
}

func integrateInvertedIndexString() map[string]string {
return map[string]string{
"key": "value", "key2": "value2", "key3": "value3", "key4": "value4", "key5": "value5",
"key6": "value6", "key7": "value7", "key8": "value8", "key9": "value9", "key10": "value10",
"key11": "value11", "key12": "value12", "key13": "value13", "key14": "value14", "key15": "value15",
"key16": "value16", "key17": "value17", "key18": "value18", "key19": "value19", "key20": "value20",
"key21": "value21", "key22": "value22", "key23": "value23", "key24": "value24",
}
}

func main() {
IndexMap := getIntegrateInvertedIndexString(1000)
for key, value := range IndexMap {
println(key, value)
}
}

报错:fatal error: concurrent map iteration and map write

这个错误其实一度让我非常困惑,为了更快的整理发送到通道的哈希表,我预先跑了一个协程,目的是让生产者和消费者同时工作,然而,程序结束之后发生如上报错。

要解释这个错误还要从channel的关闭时间以及引用变量的生命周期说起,具体我会在其他文章细说。

实际上,由于getIntegrateInvertedIndexString结束的时候,读取channel信息的生产者还没有结束运行,还在修改indexStrings,而于此同时我们在迭代IndexMap,因为IndexMapindexStrings的引用,所以两者操作的是同一个map, 这样就造成了在迭代map时候修改map的错误。

解决

至于如何解决这个问题,网上也有很多方案说明,我认为用锁,或者在逻辑上保证map不会发生并发读取就好了。