Golang:避免竞态条件

huangapple go评论85阅读模式
英文:

Golang: avoiding race conditions

问题

在Go语言中,有一些良好的实践方法可以预防竞态条件。除了不在goroutine之间共享数据之外,我还可以想到以下几点:

  1. 使用互斥锁(Mutex):通过在访问共享数据之前获取锁,并在访问完成后释放锁,可以确保同一时间只有一个goroutine可以修改数据。

  2. 使用读写锁(RWMutex):如果有多个goroutine需要读取共享数据,但只有一个goroutine需要修改数据,可以使用读写锁来提高并发性能。

  3. 使用通道(Channel):通过使用通道来进行goroutine之间的通信,可以避免竞态条件。将共享数据发送到通道中,然后由其他goroutine接收并处理数据。

  4. 使用原子操作(Atomic Operations):Go语言提供了一些原子操作函数,如atomic.AddInt64()和atomic.LoadInt64(),可以在不使用锁的情况下进行原子操作,从而避免竞态条件。

至于你提到的发送对象的深拷贝,确实可以避免竞态条件,因为每个goroutine都有自己的对象副本。但是这可能会占用更多的堆内存,并且在某些情况下可能会导致性能下降。

需要注意的是,并不是所有的情况下都可以完全避免竞态条件。即使使用了上述的预防措施,如果程序逻辑不正确或者数据依赖关系复杂,仍然可能出现竞态条件。因此,在编写并发程序时,仔细分析数据访问和修改的逻辑是非常重要的。

英文:

What are some good practices to prevent race conditions in Go?

The only one I can think of is not sharing data between goroutines - the parent goroutine sends a deep copy of an object rather than the object itself, so the child goroutine can't mutate something that the parent can. This would use up more heap memory, but the other alternative is to learn Haskell Golang:避免竞态条件

Edit: also, is there any scenario in which the method I described above can still run into race conditions?

答案1

得分: 14

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

  • B向A请求currentCount
  • C向A请求currentCount
  • B向A发送(newDataB, currentCount + 1)
  • A将newDataB存储在位置currentCount+1
  • C向A发送(newDataC, currentCount + 1)
  • A将newDataC存储在currentCount + 1的位置(覆盖newDataB;竞态条件)

这个竞态条件需要A中的私有可变状态,但不需要可变的共享数据结构,甚至不需要B或C中的可变状态。除非理解A提供的契约,否则B或C无法防止这种竞态条件。

即使在Haskell中,一旦状态进入方程式,也可能遇到这些类型的竞态条件,而且很难完全消除状态。最终,你希望程序与现实互动,而现实是有状态的。维基百科提供了一个使用STM的Haskell竞态条件示例。

我同意良好的不可变数据结构可以使事情变得更容易(Go实际上没有这些)。可变副本会将一个问题换成另一个问题。你不能意外更改其他人的数据。另一方面,你可能认为你正在更改真实的数据,但实际上只是更改了一个副本,导致不同类型的错误。无论哪种方式,你都必须理解契约。

但最终,Go在并发性方面往往遵循C的历史:为代码制定一些所有权规则(如@tux21b提供的规则),并确保始终遵循这些规则,如果你完美地做到了,一切都会很好,如果你犯了一个错误,显然是你的错,而不是语言的错。

(别误会我的意思;我很喜欢Go,真的很喜欢。它提供了一些很好的工具,使并发变得容易。但它并没有提供很多语言工具来帮助实现并发的正确性。这取决于你。尽管如此,tux21b的答案提供了很多好建议,竞态检测器绝对是减少竞态条件的强大工具。它只是不属于语言的一部分,而且它与测试有关,而不是正确性;它们不是同一回事。)

**编辑:**关于为什么不可变数据结构会使事情变得更容易的问题,这是你最初观点的延伸:创建一个多方不更改相同数据结构的契约。如果数据结构是不可变的,那么这一点就是免费的...

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

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

英文:

Race conditions can certainly still exist even with unshared data structures. Consider the following:

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)

This race condition requires private mutable state in A, but no mutable shared data structures and doesn't even require mutable state in B or C. There is nothing B or C can do to prevent this race condition without understanding the contract that A offers.

Even Haskell can suffer these kinds of race conditions as soon as state enters the equation, and state is very hard to completely eliminate from a real system. Eventually you want your program to interact with reality, and reality is stateful. Wikipedia gives a helpful race condition example in Haskell using STM.

I agree that good immutable data structures could make things easier (Go doesn't really have them). Mutable copies trade one problem for another. You can't accidentally change someone else's data. On the other hand, you may think that you're changing the real one, when you're actually just changing a copy, leading to a different kind of bug. You have to understand the contract either way.

But ultimately, Go tends to follow the history of C on concurrency: you make up some ownership rules for your code (like @tux21b offers) and make sure you always follow them, and if you do it perfectly it'll all work great, and if you ever make a mistake, then obviously it's your fault, not the language.

(Don't get me wrong; I like Go, quite a lot really. And it offers some nice tools to make concurrency easy. It just doesn't offer many language tools to help make concurrency correct. That's up to you. That said, tux21b's answer offers lots of good advice, and the race detector is definitely a powerful tool for reducing race conditions. It's just not part of the language, and it's about testing, not correctness; they're not the same thing.)

EDIT: To the question about why immutable data structures make things easier, this is the extension of your initial point: creating a contract where multiple parties don't change the same data structure. If the data structure is immutable, then that comes for free…

Many languages have a rich set of immutable collections and classes. C++ lets you const just about anything. Objective-C has immutable collections with mutable subclasses (which creates a different set of patterns than const). Scala has separate mutable and immutable versions of many collection types, and it is common practice to use the immutable versions exclusively. Declaring immutability in a method signature is an important indication of the contract.

When you pass a []byte to a goroutine, there is no way to know from the code whether the goroutine intends to modify the slice, nor when you may modify the slice yourself. There a patterns emerging, but they're like C++ object ownership before move semantics; lots of fine approaches, but no way to know which one is in use. It's a critical thing that every program needs to do correctly, yet the language gives you no good tools, and there is no universal pattern used by developers.

答案2

得分: 7

Go语言在静态上不强制执行内存安全。即使在大型代码库中,处理这个问题的方法有几种,但所有方法都需要你的注意。

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

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

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

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

Go工具还附带了一个非常有价值的竞争检测器,能够在运行时检测到竞争条件。编写大量测试,并在启用竞争检测器的情况下执行它们,并且要认真对待每个错误消息。它们通常指示了一个糟糕或复杂的设计。

(附注:如果你想要一个编译器和类型系统能够在编译时验证并发访问,同时允许共享状态,你可能想看看Rust。我自己没有使用过,但这些想法看起来非常有前途。)

英文:

Go doesn't enforce memory safety statically. There are several ways to handle the problem even in large code bases, but all of them require your attention.

  • You can send pointers around, but one common idiom is to signal the transfer of ownership by sending a pointer. E.g., once you pass the pointer of an object to another Goroutine, you do not touch it again, unless you get the object back from that goroutine (or any other Goroutine if the object is passed around several times) through another signal.

  • If your data is shared by many users and doesn't change that often, you can share a pointer to that data globally and allow everybody to read from it. If a Goroutine wants to change it, it needs to follow the copy-on-write idiom, i.e. copy the object, mutate the data, try to set the pointer to the new object by using something like atomic.CompareAndSwap.

  • Using a Mutex (or a RWMutex if you want to allow many concurrent readers at once) isn't that bad. Sure, a Mutex is no silver bullet and it is often not a good fit for doing synchronization (and its overused in many languages which lead to its poor reputation), but sometimes it is the simplest and most efficient solution.

There are probably many other ways. Sending values only by copying them is yet another and easy to verify, but I think you shouldn't limit yourself to this method only. We are all mature and we are all able to read documentation (assuming you document your code properly).

The Go tool also comes with a very valuable race detector built in, that is able to detect races at runtime. Write a lot of tests and execute them with the race detector enabled and take each error message seriously. They usually indicate a bad or complicated design.

(PS: You might want to take a look at Rust if you want a compiler and type system that is able to verify concurrent access during compile time, while still allowing shared state. I haven't used it myself, but the ideas look quite promising.)

huangapple
  • 本文由 发表于 2014年5月17日 23:42:42
  • 转载请务必保留本文链接:https://go.coder-hub.com/23713215.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定