乐趣区

关于golang:Go-并发一些有趣的现象和要避开的-坑

若有任何问题或倡议,欢送及时交换和碰撞。我的公众号是【脑子进煎鱼了】,GitHub 地址:https://github.com/eddycjy。

大家好,我是煎鱼。

最近在看 Go 并发相干的内容,发现还是有不少细节容易让人迷迷糊糊的,一个不小心就踏入深坑里,且指不定要在上线跑了一些数据后能力发现,那可真是太人解体了。

明天来分享几个案例,心愿大家在编码时可能避开这几个“坑”。

案例一

演示代码

第一个案例来自 @鸟窝 大佬在极客工夫的分享,代码如下:

func main() {
    count := 0
    wg := sync.WaitGroup{}
    wg.Add(10)
    for i := 0; i < 10; i++ {go func() {defer wg.Done()
            for j := 0; j < 100000; j++ {count++}
        }()}
    wg.Wait()

    fmt.Println(count)
}

思考一下,最初输入的 count 变量的值是多少?是不是一百万?

输入后果

在上述代码中,咱们通过 for-loop 循环起 goroutine 进行自增,并应用了 sync.WaitGroup 来保障所有的 goroutine 都执行结束才输入最终的后果值。

最终的输入后果如下:

// 第一次执行
638853

// 第二次执行
654473

// 第三次执行
786193

输入的后果值不是恒定的,也就是每次输入的都不一样,且根本不会达到设想中的一百万。

剖析起因

其起因在于 count++ 并不是一个原子操作,在汇编上就蕴含了好几个动作,如下:

MOVQ "".count(SB), AX 
LEAQ 1(AX), CX 
MOVQ CX, "".count(SB)

因为可能会同时存在多个 goroutine 同时读取到 count 的值为 1212,并各自自增 1,再将其写回。

与此同时也会有其余的 goroutine 可能也在其自增时读到了值,造成了相互笼罩的状况,这是一种并发访问共享数据的谬误。

发现问题

这类竞争问题能够通过 Go 语言所提供的的 race 检测(Go race detector)来进行剖析和发现:

$ go run -race main.go 
==================
WARNING: DATA RACE
Read at 0x00c0000c6008 by goroutine 13:
  main.main.func1()
      /Users/eddycjy/go-application/awesomeProject/main.go:28 +0x78

Previous write at 0x00c0000c6008 by goroutine 7:
  main.main.func1()
      /Users/eddycjy/go-application/awesomeProject/main.go:28 +0x91

Goroutine 13 (running) created at:
  main.main()
      /Users/eddycjy/go-application/awesomeProject/main.go:25 +0xe4

Goroutine 7 (running) created at:
  main.main()
      /Users/eddycjy/go-application/awesomeProject/main.go:25 +0xe4
==================
...
489194
Found 3 data race(s)
exit status 66

编译器会通过探测所有的内存拜访,监听其内存地址的拜访(读或写)。在利用运行时就可能发现对共享变量的拜访和操作,进而发现问题并打印出相干的正告信息。

须要留神的一点是,go run -race 是运行时检测,并不是编译时。且 race 存在明确的性能开销,通常是失常程序的十倍,因而不要想不开在生产环境关上这个配置,很容易翻车。

案例二

演示代码

第二个案例来自煎鱼在脑子的分享,代码如下:

func main() {wg := sync.WaitGroup{}
    wg.Add(5)
    for i := 0; i < 5; i++ {go func(i int) {defer wg.Done()
            fmt.Println(i)
        }(i)
    }
    wg.Wait()}

思考一下,最初输入的后果是什么?值都是 4 吗?输入是稳固有序的吗?

输入后果

在上述代码中,咱们通过 for-loop 循环起了多个 goroutine,并将变量 i 作为形参传递给了 goroutine,最初在 goroutine 内输入了变量 i

最终的输入后果如下:

// 第一次输入
0
1
2
4
3

// 第二次输入
4
0
1
2
3

显然,从后果上来看,输入的值都是无序且不稳固的,值更不是 4。这到底是为什么?

剖析起因

其起因在于,即便所有的 goroutine 都创立完了,但 goroutine 不肯定曾经开始运行了。

也就是等到 goroutine 真正去执行输入时,变量 i(值拷贝)可能曾经不是创立时的值了。

其整个程序扭转本质上分为了多个阶段,也就是各自运行的工夫线并不同,能够其拆分为:

  • 先创立:for-loop 循环创立 goroutine
  • 再调度:协程goroutine 开始调度执行。
  • 才执行:开始执行 goroutine 内的输入。

同时 goroutine 的调度存在肯定的随机性(倡议理解一下 GMP 模型),那么其输入的后果就势必是无序且不稳固的。

发现问题

这时候你可能会想,那后面提到的 go run -race 能不能发现这个问题呢。如下:

$ go run -race main.go
0
1
2
3
4

没有呈现正告,显然是不能的,因为其本质上并不是并发访问共享数据的谬误,且会导致程序变成了串行,从而蒙蔽了你的双眼。

案例三

演示代码

第三个案例来自煎鱼在梦里的分享,代码如下:

func main() {wg := sync.WaitGroup{}
    wg.Add(5)
    for i := 0; i < 5; i++ {go func() {defer wg.Done()
            fmt.Println(i)
        }()}
    wg.Wait()}

思考一下,最初输入的后果是什么?值都是 4 吗?会像案例二一样乱窜吗?

输入后果

在上述代码中,与案例二大体没有区别,次要是变量 i 没有作为形参传入。

最终的输入后果如下:

// 第一次输入
5
5
5
5
5

初步从输入的后果上来看都是 5,这时候就会有人迷糊了,为什么不是 4 呢?

不少人会因不是 4 而陷入了蛊惑,但千万不要被一两次的输入蛊惑了心智,认为铁定就是 5 了。能够再入手多输入几次,如下:

// 多输入几次
5
3
5
5
5

最终会发现 … 输入后果存在随机性,输入后果并不是 100% 都是 5,更不必提 4 了。这到底是为什么呢?

剖析起因

其起因与案例二其实十分靠近,实践上了解了案例二也就能解决案例三。

其本质还是创立 goroutine 与真正执行 fmt.Println 并不同步。因而很有可能在你执行 fmt.Println 时,循环 for-loop 曾经运行结束,因而变量 i 的值最终变成了 5。

那么相同,其也有可能没运行完,存在随机性。写个 test case 就能发现显著的不同。

总结

在本文中,我分享了几个近期看到次数最频繁的一些并发上的小“坑”,心愿对你有所帮忙。同时你也能够回忆一下,在你编写 Go 并发程序有没有也遇到过什么问题?

同时你也能够回忆一下,在你编写 Go 并发程序有没有也遇到过什么问题?

我的公众号

分享 Go 语言、微服务架构和奇怪的零碎设计,欢送大家关注我的公众号和我进行交换和沟通。

最好的关系是相互成就 ,各位的 点赞 就是煎鱼创作的最大能源,感激反对。

退出移动版