Go设置单线程运行,是否还会有并发问题

Go 程序单线程多 goroutine 访问一个 map 会遇到并发读写 panic 么?

如果并发单元同时执行,即使是单线程,可能就会产生数据竞争的问题,除非这些 goroutine 是顺序执行的

所以

Golang中的map是否线程安全?

答:Golang中的map不是线程安全的,因为多个Goroutine同时读写同一个map会产生竞争条件。可以通过加锁或使用sync.Map等线程安全的数据结构来进行解决。

其实可以改成不是协程安全的, 即便只有单个线程(人工限制或者机器只有单核), 也可能会有并发问题

所以 多个Goroutine同时读写同一个map会产生竞争条件,不用管这些goroutine是来自一个核还是多个核(来自同一个GPM,还是不同的GPM)

爽哥总结: Go单线程情况仍然可能有并发冲突,因为线程和协程是m:n的关系,当线程m=1,协程数量可能为x,且x>=1. 这时候并发读写map就有可能造成冲突..因为实际去读写的是协程而不是线程.




奋哥: 刚刚看到一个cr, 是修复服务挂掉的, 触发原因是容器升级配置挂掉了(升级CPU核数). 触发的直接原因,数据有个并发map读写.
原来一个核的时候, 执行go代码无并发冲突, 升配就有了. recover也recover不住.gomaxprocs是uber库自动计算的

其他人: 这不冷门吧。。某种角度来讲。这甚至是go官方文档提及的部分

奋哥: 其实就是触发条件比较有意思..代码跑的好好的, 跑了这么久了, 流量也不低, 咋升级配置就挂掉了呢


曹大: 并发不是并行,一个核也可以触发,0.5都可以

奋哥: 自动设置procs的,一个核就是不会map panic ¯_(ツ)_/¯

GongXun: 这波我站奋哥[破涕为笑]。

曹大: go 看起来是这样

Go单线程运行也会有并发问题

鸟窝老师完整的代码:

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
73
74
75
76
77
78
79
80
81
82
83
package main

import (
"fmt"
"runtime"
"sync"
)

func TestCounter() {
runtime.GOMAXPROCS(1)

var counter int

var wg sync.WaitGroup
wg.Add(10)
for i := 0; i < 10; i++ {
i := i
go func() {
fmt.Printf("start task#%d, counter: %d\n", i, counter)
for j := 0; j < 10_0000; j++ {
counter++
}
fmt.Printf("end task#%d, counter: %d\n", i, counter)
wg.Done()
}()
}

wg.Wait()
fmt.Println(counter)
}

func TestCounter2() {
runtime.GOMAXPROCS(1)

var counter int

var wg sync.WaitGroup
wg.Add(10)
for i := 0; i < 10; i++ {
i := i
go func() {
fmt.Printf("start task#%d, counter: %d\n", i, counter)
for j := 0; j < 10_0000; j++ {
temp := counter
runtime.Gosched()
temp = temp + 1
counter = temp
}
fmt.Printf("end task#%d, counter: %d\n", i, counter)
wg.Done()
}()
}

wg.Wait()
fmt.Println(counter)
}

func TestMap() {
runtime.GOMAXPROCS(1)
var m = make(map[int]int)
var wg sync.WaitGroup
wg.Add(10)
for i := 0; i < 10; i++ {
i := i
go func() {
fmt.Printf("start map task#%d, m: %v\n", i, len(m))
for j := 0; j < 10_0000; j++ {
m[j] = i*10_0000 + j
}
fmt.Printf("end map task#%d, m: %v\n", i, len(m))
wg.Done()
}()
}

wg.Wait()
}

func main() {
//TestCounter()
//TestCounter2()
TestMap()
}


GongXun: 单核并发map也有问题,它检查的是flag,也是顺序性问题。

曹大: 还是鸟窝大佬牛,我刚才试了一下都没构造出来. 认怂太快了

奋哥: 我测了下, 加上一些耗时模拟了下, 的确还是会panic. 应该这么说, 之前的单GOMAXPROCS跑, 不是不会挂, 而是出现挂的概率很小, 出现挂一次时间很久很久.

其他人: 不过单核map读,写,应该不会在读写函数里面被抢占,然后调度出去吧,例子中的循环太大了,是否是信号抢占

奋哥: 因为这种是未定义行为, 涉及到调度顺序, 抢占, 还有map的检测, 我设置了下GOMAXPROCS为1, 没太搞明白为啥会挂. 但我发现一个事情, proc为1, 在我mac上跑, 总是在一个协程跑了49150次左右时, panic. 不管设置map后有没有加sleep

其他人: 抢占需要10ms,也可能前面的代码运行了比较长的时间,到map刚好被抢占

奋哥: 我也考虑了抢占问题, 但我给协程加了sleep, 也还是这样

其他人: 可以把异步抢占禁用了验证下

奋哥: 验过了. 还是会panic

xiaorui: 我关了。还是 panic 。奇怪。

奋哥: map咋检测我没太关注细节, 这个可能涉及到GOMAXPROCS的机制, run next, 抢占, 然后还和go版本有关.
所以未定义行为, 需要深入看看源码..

其他人: 就是里面设置了个 flag,操作的时候检测下 flag ,非常朴素


FB: GOMAXPROCS 1 是os只有一个线程跑go 线程。这个没理解错吧

曹大: 写半天高并发,基本原理都搞不懂. 还是写crud吧,只在一个 request 里活动

FB: GMP就不一样了吧。一个os thread可能跑go thread N个。


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

import (
"fmt"
"sync"
"time"
)
func main() {
var m = make(map[int]int)
var wg sync.WaitGroup
wg.Add(10)
for i := 0; i < 10; i++ {
i := i
go func() {
fmt.Printf("start map task#%d, m: %v\n", i, len(m))
for j := 0; j < 10_0000; j++ {
m[j] = i*10_0000 + j
time.Sleep(time.Millisecond * 15)
}
fmt.Printf("end map task#%d, m: %v\n", i, len(m))
wg.Done()
}()
}

wg.Wait()
}

奋哥: 是不是49151?

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

import (
"fmt"
"sync"
"time"
"runtime"
)
func main() {
runtime.GOMAXPROCS(1)
var m = make(map[int]int)
var wg sync.WaitGroup
wg.Add(10)
for i := 0; i < 10; i++ {
i := i
go func() {
fmt.Printf("start map task#%d, m: %v\n", i, len(m))
for j := 0; j < 10_0000; j++ {
m[j] = i*10_0000 + j
time.Sleep(time.Millisecond * 15)
}
fmt.Printf("end map task#%d, m: %v\n", i, len(m))
wg.Done()
}()
}

wg.Wait()
}

单个核跑了蛮久没崩

奋哥: 哈哈. 未定义行为, 随版本还会变更..我发现和cpu, 操作系统没关系.
我用1.21, 单个协程跑49152挂, 不管在mac arm64还是linux amd64上.1.17则是53248挂

其他人: 调度间隔/抢占时机 变了?

奋哥: 加了time sleep也是这个计数..和抢占没关系. 我感觉还是和map里的检测机制有关系..或者不同版本的map改了某个参数

FB: 确实跑到49152就挂了

1
2
3
start map task#9, m: 0
start map task#0, m: 26643
start map task#1, m: 53248

go1.18.1

https://golang.org/ref/mem

奋哥: 1. 为啥1个proc, 会挂呢, 我也加了sleep了
2. 为啥同一个版本都是跑到同一个数挂掉?….我感觉还是和map本身的检测机制有关

https://stackoverflow.com/questions/56780384/is-it-possible-to-read-half-written-corrupt-primitive-variable-when-using-multi

https://lugosix.github.io/blog/post/memory_model/memory-model/

文章目录