当两个或更多的操作必须以正确的顺序执行时,就会出现竞争状态,但如果程序没有写入,无法使操作顺序得到保持。

大多数时候,这出现在所谓的数据竞争中,其中一个并发操作尝试在某些未确定的时间读取变量,而另一个并发操作尝试写入同一个变量。

这里有一个简单的例子:

1 var data int
2 go func() { // 1
3     data++
4 }()
5 if data == 0 {
6     fmt.Printf("the value is %v.\n", data)
7 }
  1. 在Go中,可以使用go关键字同时运行一个函数。 这样做创建了所谓的goroutine。

在第3行和第5行都试图访问名为data的变量,但是并没有施行任何措施保证执行的顺序。运行此代码有三种可能的结果:

  • 没有输出。在这种情况下,第3行是在第5行之前执行的。
  • 输出 the value is 0。在这种情况下,第5行和第6行在第3行之前执行。
  • 输出 the value is 1。在这种情况下,第5行在第3行之前执行,但第3行在第6行之前执行。

正如你所看到的,仅仅几行不确定的代码会在你的程序中引入巨大的变化。

大多数情况下,数据竞争是由于开发人员按顺序思考问题而引入的。 他们认为,上一行代码会先于下一行代码执行。 他们假设在if语句中读取数据变量之前,上面的goroutine将被调度并执行。

在编写并发代码时,你必须仔细地遍历所有可能出现的场景。 除非你正在使用本书稍后部分介绍的一些技巧,否则保证代码将按其在源代码中列出的顺序运行。 我有时会发现在操作之间等待很长一段时间会很有帮助。 想象一下,在调用goroutine的时间和运行的时间之间要经过一个小时。 该程序的其余部分如何运作? 如果在goroutine成功执行和程序到达if语句之间花了一个小时呢? 以这种方式思考对我有所帮助,因为对于计算机而言,规模可能不同,但相对时间差异差不多。

事实上一些开发者确实这么干并发现看起来解决了并发上的问题,我们修改上个例子看看:

1 var data int
2 go func() { // 1
3     data++
4 }()
5 time.Sleep(1*time.Second) // 这种做法实在太烂了!
6 if data == 0 {
7     fmt.Printf("the value is %v.\n", data)
8 }

我们解决了数据竞争问题吗吗?没有。事实上,从这个方案中产生的所有三个结果仍然是可能的。我们在调用我们的goroutine和检查数据值之间的让程序休眠的时间越长,程序越接近实现正确性——但这只是在概率上渐近地接近逻辑正确而已。

除此之外,这样做已经在算法中引入了低效率。 程序现在必须休眠一秒钟才能使我们更有可能看不到的数据竞争。如果我们使用正确的方式来编写代码,我们可能无需等待,或者等待时间可能只有1微秒。

这里要说的是,你应该总是以逻辑的正确性为目标。 在你的代码中引入休眠可以是一种调试并发程序的方便方式,但不是解决方案。

数据竞争的产生条件是最隐秘的并发错误类型之一,因为它们可能在代码投入生产后才会展现出来。 它们通常是由代码执行环境发生变化或前所未有的突发事件引起的。 在这些情况下,代码看起来行为正确,但实际上,这些操作按顺序执行的出现不确定性的几率非常高。

最后编辑: kuteng  文档更新时间: 2021-01-02 17:30   作者:kuteng