【问题标题】:Golang: avoiding race conditions [closed]Golang:避免竞争条件[关闭]
【发布时间】:2014-05-17 15:42:42
【问题描述】:

有哪些好的做法可以防止 Go 中的竞争条件?

我唯一能想到的就是不在 goroutine 之间共享数据——父 goroutine 发送对象的深层副本而不是对象本身,因此子 goroutine 不能改变父 goroutine 可以改变的东西。这会占用更多的堆内存,但另一种选择是学习 Haskell:P

编辑:另外,有没有我上面描述的方法仍然会遇到竞争条件的情况?

【问题讨论】:

  • 竞态条件很难调试,也很难掌握避免它们。我建议考虑 Rob Pike vimeo.com/49718712 在这里描述的模式,因为他通过某些构造来了解通道和 CSP 语义如何通过设计使应用程序安全,而不是担心与互斥锁相关的所有问题。很抱歉,如果这不能回答您的问题,但我希望它为新想法打开更多大门。
  • 感谢您的链接。是的,竞争条件很难预防,但更难调试!我想集合的不可变版本会在一定程度上缓解这个问题,但是我们又回到了函数式编程的后院。
  • 还有 其他基于不可变/函数式的语言(例如 F#),但它确实与风格有关。需要更多内存是对的,但适当的设计可以消除一些开销。例如,写入修改数组平均可以在n log n 空间(或对于某些潜在情况更少)完成,而不是天真的2n。尽管语言/优化器支持可以为专用语言做一些疯狂的事情......

标签: go race-condition deep-copy goroutine


【解决方案1】:

即使使用非共享数据结构,竞争条件当然仍然存在。考虑以下几点:

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 无法阻止这种竞争条件一个优惠。

一旦状态进入方程,即使是 Haskell 也会遭受这些竞争条件,而状态很难从真实系统中完全消除。最终,您希望您的程序与现实交互,而现实是有状态的。维基百科使用 STM 给出a helpful race condition example in Haskell。

我同意好的不可变数据结构可以使事情变得更容易(Go 并没有它们)。可变副本以一个问题换另一个问题。您不会意外更改他人的数据。另一方面,您可能认为您正在更改真实的,而实际上您只是更改副本,从而导致不同类型的错误。无论哪种方式,您都必须了解合同。

但归根结底,Go 倾向于遵循 C 在并发方面的历史:你为你的代码制定一些所有权规则(如 @tux21b 提供的)并确保你始终遵循它们,如果你做得很好,一切都会工作得很好,如果你犯了错误,那么显然是你的错,而不是语言。

(不要误会我的意思;我非常喜欢 Go,非常喜欢。它提供了一些很好的工具来简化并发。它只是没有提供很多语言工具来帮助实现并发正确 em>。这取决于你。也就是说,tux21b 的答案提供了很多很好的建议,并且竞争检测器绝对是减少竞争条件的强大工具。它不是语言的一部分,它是关于测试,而不是正确性;他们'不是一回事。)

编辑:关于为什么不可变数据结构使事情变得更容易的问题,这是您最初观点的扩展:创建一个多方不更改相同数据结构的合同。如果数据结构是不可变的,那么它是免费的……

许多语言都有一组丰富的不可变集合和类。 C++ 让你const 几乎任何东西。 Objective-C 具有带有可变子类的不可变集合(它创建了一组与const 不同的模式)。 Scala 有许多集合类型的独立可变和不可变版本,通常的做法是专门使用不可变版本。在方法签名中声明不变性是合同的重要指示。

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

【讨论】:

  • 这是一个很好的答案。您能否详细说明不可变数据结构如何使事情变得更容易?
【解决方案2】:

Go 不会静态地强制执行内存安全。即使在大型代码库中,也有多种方法可以处理该问题,但所有这些方法都需要您注意。

  • 您可以发送指针,但一个常见的习惯用法是通过发送指针来表示所有权转移。例如,一旦你将一个对象的指针传递给另一个 Goroutine,你就不会再触摸它,除非你通过另一个信号从那个 Goroutine(或任何其他 Goroutine,如果对象被多次传递)取回对象。

  • 如果您的数据由许多用户共享并且不经常更改,您可以在全局范围内共享指向该数据的指针并允许每个人从中读取。如果一个 Goroutine 想要改变它,它需要遵循写时复制的习惯用法,即复制对象,改变数据,尝试使用 atomic.CompareAndSwap 之类的方法将指针设置为新对象。

  • 使用 Mutex(如果您想同时允许多个并发读取器,则使用 RWMutex)并没有那么糟糕。当然,Mutex 不是灵丹妙药,它通常不适合进行同步(它在许多语言中被过度使用,导致其声誉不佳),但有时它是最简单和最有效的解决方案。

可能还有很多其他方法。仅通过复制它们来发送值是另一种易于验证的方法,但我认为您不应该仅将自己限制在这种方法上。我们都很成熟,我们都能够阅读文档(假设您正确地记录了您的代码)。

Go 工具还内置了一个非常有价值的race detector,它能够在运行时检测比赛。编写大量测试并在启用竞争检测器的情况下执行它们,并认真对待每条错误消息。它们通常表示设计不佳或复杂。

(PS:如果你想要一个能够在编译期间验证并发访问,同时仍然允许共享状态的编译器和类型系统,你可能想看看Rust。我自己没有使用过,但这些想法看起来很有希望。)

【讨论】:

  • 这些都是有效的点,如果我自己编写整个代码,它们会起作用。但是,如果我的 goroutine 正在运行来自外部库的函数,我必须通过它的实现来确保库的开发人员也遵守合同。我认为不分享任何状态会使我更容易将自己与任何外部因素隔离开来。是否存在我提出的解决方案仍可能遇到竞争条件的情况?
猜你喜欢
  • 1970-01-01
  • 2015-01-30
  • 2010-09-25
  • 2010-09-25
  • 2019-06-12
  • 1970-01-01
  • 2020-01-16
  • 2014-04-02
相关资源
最近更新 更多