Go语言单线程运行也会有的并发问题解析

 更新时间:2023年12月20日 09:22:41   作者:晁岳攀(鸟窝) 鸟窝聊技术  
这篇文章主要为大家介绍了Go语言单线程运行的并发问题解析,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪

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

一个 Go 大佬群中严肃的讨论了一个问题:Go 程序单线程多 goroutine 访问一个 map 会遇到并发读写 panic 么?

答案是肯定的,因为出现了这个问题所以大家才在群中讨论。

为什么呢?因为单线程意味着并行单元只有一个(多线程也可能并行单元只有一个),但是多 goroutine 意味着并发单元有多个,如果并发单元同时执行,即使是单线程,可能就会产生数据竞争的问题,除非这些 goroutine 是顺序执行的。

举一个例子哈:

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)
}

这段测试代码是启动 10 个 goroutine 对计数器加一,每个 goroutine 负责加 10 万次。在我的 MBP m1 笔记本上,每次的结果都是 100 万,符合期望。如果你运行这段代码,会发现 goroutine 其实是一个一个串行执行的(9->0->1->2->3->4->5->6->7->8,当然可能在你的机器上不是这样的),如果是串行执行,不会有并发问题:

start task#9, counter: 0
end task#9, counter: 100000
start task#0, counter: 100000
end task#0, counter: 200000
start task#1, counter: 200000
end task#1, counter: 300000
start task#2, counter: 300000
end task#2, counter: 400000
start task#3, counter: 400000
end task#3, counter: 500000
start task#4, counter: 500000
end task#4, counter: 600000
start task#5, counter: 600000
end task#5, counter: 700000
start task#6, counter: 700000
end task#6, counter: 800000
start task#7, counter: 800000
end task#7, counter: 900000
start task#8, counter: 900000
end task#8, counter: 1000000
1000000

插入runtime.Gosched()

为了制造点紧张气氛,我将代码改写成下面这样子,将counter++三条指令明显写成三条语句,并在中间插入runtime.Gosched(),故意给其它 goroutine 的执行制造机会:

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)
}

运行这段代码,你就会明显看到数据不一致的效果,即使是单个线程运行 goroutine,也出现了数据竞争的问题:

start task#9, counter: 0
start task#0, counter: 0
start task#1, counter: 0
start task#2, counter: 0
start task#3, counter: 0
start task#4, counter: 0
start task#5, counter: 0
start task#6, counter: 0
start task#7, counter: 0
start task#8, counter: 0
end task#9, counter: 100000
end task#1, counter: 100000
end task#3, counter: 100000
end task#2, counter: 100000
end task#5, counter: 100000
end task#0, counter: 100000
end task#4, counter: 100000
end task#6, counter: 100000
end task#7, counter: 100000
end task#8, counter: 100000
100000

这个结果非常离谱,期望 100 万,最后只有 10 万。

访问同一个 map对象也有可能出现并发bug

因为单个线程运行多个 goroutine 会有数据竞争的问题,所以访问同一个 map 对象也有可能出现并发 bug,比如下面的代码,10 个 goroutine 并发的写同一个 map:

func TestMap() {
 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()
}

大概率会出现 panic:

start map task#9, m: 0
start map task#0, m: 49152
fatal error: concurrent map writes
goroutine 41 [running]:
main.TestMap.func1()
 /Users/chaoyuepan/study/single_thread/main.go:72 +0xcc
created by main.TestMap in goroutine 1
 /Users/chaoyuepan/study/single_thread/main.go:69 +0x4c
goroutine 1 [semacquire]:
sync.runtime_Semacquire(0x140000021a0?)
 /usr/local/go/src/runtime/sema.go:62 +0x2c
sync.(*WaitGroup).Wait(0x1400000e1d0)
 /usr/local/go/src/sync/waitgroup.go:116 +0x74
main.TestMap()
 /Users/chaoyuepan/study/single_thread/main.go:79 +0xb8
main.main()
 /Users/chaoyuepan/study/single_thread/main.go:15 +0x2c

以上就是Go语言单线程运行的并发问题解析的详细内容,更多关于Go单线程运行并发问题的资料请关注脚本之家其它相关文章!

相关文章

  • golang 获取明天零点的时间戳示例

    golang 获取明天零点的时间戳示例

    今天小编就为大家分享一篇golang 获取明天零点的时间戳示例,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2018-05-05
  • Go语言微服务中实现链路追踪

    Go语言微服务中实现链路追踪

    在微服务架构中,链路追踪技术可以帮助我们跟踪请求在各个服务之间的传播路径,本文就来介绍一下Go语言微服务中实现链路追踪,感兴趣的可以了解一下
    2024-12-12
  • golang db事务的统一封装的实现

    golang db事务的统一封装的实现

    这篇文章主要介绍了golang db事务的统一封装的实现,文中通过示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2021-12-12
  • Golang负载均衡和保活设计原理示例探究

    Golang负载均衡和保活设计原理示例探究

    这篇文章主要为大家介绍了Golang负载均衡和保活设计原理示例探究,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2024-01-01
  • Golang sync.Map原理深入分析讲解

    Golang sync.Map原理深入分析讲解

    go中map数据结构不是线程安全的,即多个goroutine同时操作一个map,则会报错,因此go1.9之后诞生了sync.Map,sync.Map思路来自java的ConcurrentHashMap
    2022-12-12
  • 详解Go语言如何实现二叉树遍历

    详解Go语言如何实现二叉树遍历

    这篇文章主要为大家详解介绍了Go语言中如何实现二叉树遍历,文中的示例代码讲解详细,对我们学习Go语言有一定帮助,需要的可以参考一下
    2022-04-04
  • Go 并发读写 sync.map 详细

    Go 并发读写 sync.map 详细

    阅读本文你将会明确 sync.Map 和原生 map +互斥锁/读写锁之间的性能情况。标准库 sync.Map 虽说支持并发读写 map,但更适用于读多写少的场景,因为他写入的性能比较差,使用时要考虑清楚这一点。
    2021-10-10
  • 揭秘Go Json.Unmarshal精度丢失之谜

    揭秘Go Json.Unmarshal精度丢失之谜

    我们知道在json反序列化时是没有整型和浮点型的区别,数字都使用同一种类型,在go语言的类型中这种共同类型就是float64,下面我们就来探讨一下Json.Unmarshal精度丢失之谜吧
    2023-08-08
  • GoFrame框架ORM原生方法对象操作开箱体验

    GoFrame框架ORM原生方法对象操作开箱体验

    这篇文章主要为大家介绍了GoFrame框架ORM原生方法对象操作的开箱体验,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-06-06
  • 如何利用Golang解析读取Mysql备份文件

    如何利用Golang解析读取Mysql备份文件

    这篇文章主要给大家介绍了关于如何利用Golang解析读取Mysql备份文件的相关资料,文中通过示例代码介绍的非常详细,对大家学习或者使用Golang具有一定的参考学习价值,需要的朋友们下面来一起学习学习吧
    2019-12-12

最新评论