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 mainimport ( "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 () { 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 mainimport ("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/
原文链接: https://dashen.tech/2023/12/11/Go设置单线程运行,是否还会有并发问题/
版权声明: 转载请注明出处.