DarkCodeMaster's Blog.

Go Map并发问题解决方案

Word count: 721Reading time: 3 min
2022/07/29

构造并发操作场景

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main

func main() {
m := make(map[int]int)
go func() {
for {
_ = m[1]
}
}()
go func() {
for {
m[0] = 1
}
}()
select {}
}
// 输出
// fatal error: concurrent map read and map write

go的map即使读写的不是一个建, 只要存在同时间访问就会出现并发读写的异常

解决方案

1. 使用读写锁

这里我们使用读写锁来保证Map的并发安全, 当然使用互斥锁也行, 但是当读操作多的时候性能会差一些,
因为读锁和读锁不互斥所以锁竞争相对小

操作要点

  1. 读的时候加读锁, 读完解锁
  2. 写的时候加写锁, 写完解锁

代码实现

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

import "sync"

type MapWithLock struct {
data map[int]int
sync.RWMutex
}

func NewMapWithLock() *MapWithLock {
mapWithLock := new(MapWithLock)
mapWithLock.data = make(map[int]int)
return mapWithLock
}

func (m *MapWithLock) Set(key, value int) {
m.Lock()
m.data[key] = value
m.Unlock()
}

func (m *MapWithLock) Get(key int) int {
m.RLock()
value := m.data[key]
m.RUnlock()
return value
}

func main() {
m := NewMapWithLock()
go func() {
for {
_ = m.Get(1)
}
}()
go func() {
for {
m.Set(0, 1)
}
}()
select {}
}

2. 使用channel

使用channel线程安全的特性来保证Map的操作是串行的

操作要点

  1. 无缓存的channel在写入的时候只能写入一个, 另一个写入操作会被阻塞
  2. 在创建map的时候我们启动一个Goroutine来读取channel里面的操作
  3. 在读的时候需要添加等待组, 不然可能会拿到空值 (原因是没等函数执行完, 就return了)

代码实现

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
71
72
package main

import (
"fmt"
"sync"
)

type MapWithChannel struct {
data map[int]int
ch chan func()
}

func NewMapWithChannel() *MapWithChannel {
mapWithChannel := new(MapWithChannel)
mapWithChannel.data = make(map[int]int)
mapWithChannel.ch = make(chan func())
mapWithChannel.readChannel()
return mapWithChannel
}

func (m *MapWithChannel) set(key, value int) {
m.data[key] = value
}

func (m *MapWithChannel) get(key int) int {
value := m.data[key]
return value
}

func (m *MapWithChannel) Set(key, value int) {
m.ch <- func() {
m.set(key, value)
}
}

func (m *MapWithChannel) Get(key int) (value int) {
wg := sync.WaitGroup{}
wg.Add(1)
m.ch <- func() {
value = m.get(key)
wg.Done()
}
wg.Wait()
return value
}

func (m *MapWithChannel) readChannel() {
go func() {
for {
f := <-m.ch
f()
}
}()
}

func main() {
m := NewMapWithChannel()
go func() {
for {
x := m.Get(0)
fmt.Println(x)
}
}()
go func() {
for {
m.Set(0, 1)
}
}()
select {}
}


3. 使用sync.Map

sync.Map 是线程安全的, 可以大胆使用, 但是在写操作较多的时候性能不太好, 建议读操作多的时候使用,
写操作多的话还是老老实实加锁

操作要点

  1. sync.Map 的键和值都是interface{}类型的, 使用的时候需要进行类型断言
    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 (
    "fmt"
    "sync"
    )

    func main() {
    m := sync.Map{}
    go func() {
    for {
    v, ok := m.Load(0)
    if !ok {
    break
    }
    value, ok := v.(int)
    if !ok {
    break
    }
    fmt.Println(value)
    }
    }()
    go func() {
    for {
    m.Store(0, 1)
    }
    }()
    select {}
    }


目录
  1. 1. 构造并发操作场景
  2. 2. 解决方案
    1. 2.1. 1. 使用读写锁
      1. 2.1.1. 操作要点
      2. 2.1.2. 代码实现
    2. 2.2. 2. 使用channel
      1. 2.2.1. 操作要点
      2. 2.2.2. 代码实现
    3. 2.3. 3. 使用sync.Map
      1. 2.3.1. 操作要点