Golang:避免竞态条件

8

有哪些好的实践可以在Go中防止竞态条件?

我所知道的唯一方法是不在goroutine之间共享数据-父goroutine发送对象的深拷贝而不是对象本身,这样子goroutine就无法改变父goroutine可操作的东西。这将使用更多的堆内存,但另一种选择是学习Haskell:P

编辑:还有,是否存在任何场景,即使使用我上面描述的方法仍可能遇到竞态条件?


1
竞争条件很难调试,避免它们也很难掌握。我建议考虑Rob Pike在这里描述的模式http://vimeo.com/49718712,因为他通过某些构造来介绍通道和CSP语义如何通过设计使应用程序安全,而不是担心与mutex相关的所有问题。如果我的回答没有解决您的问题,我希望它能为您打开新思路。 - ymg
谢谢提供链接。是的,竞态条件很难预防,但更难调试!我猜想,如果使用不可变版本的集合,可以在一定程度上缓解这个问题,但这样我们又回到了函数式编程的领域。 - tldr
还有其他基于不可变性/函数式的语言(如F#),但这真的是关于风格的问题。你说得对,需要更多的内存,但适当的设计可以消除一些开销。例如,修改时写入数组可以在平均n log n空间(或某些潜在情况下更少)上完成,而不是天真的2n。虽然语言/优化器支持可以为专门构建的语言做一些疯狂的事情... - Clockwork-Muse
2个回答

14

即使使用未共享的数据结构,竞态条件仍然可能存在。请考虑以下情况:

B asks A for the currentCount
C asks A for the currentCount
B sends A (newDataB, currentCount + 1)
A stores newDataB at location currentCount+1
C sends A (newDataC, currentCount + 1)
A stores newDataC at currentCount + 1 (overwriting newDataB; race condition)

这种竞争条件需要在A中拥有私有的可变状态,但不需要可变的共享数据结构,甚至不要求B或C中存在可变状态。没有什么B或C可以做来防止这种竞争条件,除非他们理解A提供的约定。

即使是Haskell只要涉及状态就可能遭受这些类型的竞争条件,而从现实系统中完全消除状态非常困难。最终您希望程序与现实交互,并且现实是有状态的。Wikipedia给出了使用STM的Haskell竞争条件示例

我同意好的不可变数据结构可以使事情变得更容易(Go实际上没有这样的数据结构)。可变副本会把一个问题换成另一个问题。您无法意外更改其他人的数据。另一方面,您可能会认为正在更改真实的数据,而实际上只是更改了副本,导致不同类型的错误。您必须以任何方式都要理解约定。

但最终,Go倾向于跟随C的并发历史:您为代码制定一些所有权规则(例如@tux21b提供的规则),并确保您始终遵循它们。如果您完美地做到了这一点,一切都会很好。如果您犯了一个错误,那显然是您的错,而不是语言的错。

(别误会我;我很喜欢Go。它提供了一些很好的工具,使并发变得容易。但它没有提供太多的语言工具来帮助实现并发的正确性。这取决于您。尽管如此,tux21b的答案提供了许多好的建议,竞争检测器确实是减少竞争条件的有力工具,只是它不是语言的一部分,而是关于测试的,而不是正确性。它们并不相同。)

编辑:关于为什么不可变数据结构可以使事情变得更容易的问题,这是你最初观点的延伸:创建一个约定,多个方不更改同一数据结构。如果数据结构是不可变的,则可以轻松实现该约定...

许多语言都拥有丰富的不可变集合和类。C++允许您const几乎任何东西。Objective-C具有具有可变子类的不可变集合(这创建了与const不同的模式)。Scala针对许多集合类型有单独的可变和不可变版本,并且使用不可变版本是常见做法。在方法签名中声明不可变性是约定的重要指示。

当您将[]byte传递给协程时,无法从代码中知道协程是否打算修改该片段,也不知道何时可以自己修改该片段。此时出现了一些模式,但它们就像移动语义之前的C++对象所有权一样;有很多好的方法,但没有办法知道哪种方法正在使用。这是每个程序都需要正确执行的关键事情,然而语言并没有给您提供好的工具,并且开发人员也没有普遍采用通用模式。


1
这是一个很好的答案。您能详细说明一下不可变数据结构如何使事情更容易吗? - tldr

7
Go语言不会在静态情况下强制执行内存安全。即使在大型代码库中,有几种处理问题的方式,但所有这些方式都需要您的注意。
- 您可以发送指针,但一种常见的习惯是通过发送指针来表示所有权的转移。例如,一旦将对象的指针传递给另一个Goroutine,除非通过另一个信号从该goroutine(或任何其他Goroutine,如果对象被多次传递)中获取对象,否则不再触摸它。 - 如果您的数据由许多用户共享并且不经常更改,则可以全局共享指向该数据的指针,并允许每个人都从中读取。如果Goroutine想要更改它,则需要遵循写时复制习惯,即复制对象,突变数据,尝试使用类似atomic.CompareAndSwap的东西将指针设置为新对象。 - 使用Mutex(如果要同时允许许多并发读取器,则使用RWMutex)并不那么糟糕。当然,Mutex不是万能的,它通常不适合进行同步(在许多语言中过度使用导致其声誉不佳),但有时它是最简单和最有效的解决方案。
也许还有其他的方式。仅通过复制来发送值是另一种易于验证的方法,但我认为你不应该仅限于此方法。我们都很成熟并且能够阅读文档(假设你正确地记录了你的代码)。
Go工具还附带了一个非常有价值的竞态检测器,可以在运行时检测到竞争问题。编写大量测试,并启用竞态检测器运行测试,并认真对待每个错误信息。它们通常指示了一个不良或复杂的设计。
(附注:如果你想要一个能够在编译时验证并发访问的编译器和类型系统,同时允许共享状态,你可能想看看Rust。 我自己没有使用过它,但这些想法看起来相当有前途。)

这些都是有效的观点,如果我自己编写整个代码,它们会起作用。但是,如果我的 goroutine 运行来自外部库的函数,则必须通过其实现来确保库的开发人员也遵守合同。我认为不共享任何状态使我更容易隔离自己免受任何外部影响。是否存在任何情况,使得我的提议仍可能遇到竞态条件? - tldr

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接