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 :P
Edit: also, is there any scenario in which the method I described above can still run into race conditions?
We will use a Mutex to prevent the race condition. We know the data race occurs due to multiple goroutines accessing a shared variable. We can avoid this by locking access to our counter variable when one of the goroutines reads it, and then unlocking it when it is done writing the incremented value.
The usual solution to avoid race condition is to serialize access to the shared resource. If one process gains access first, the resource is "locked" so that other processes have to wait for the resource to become available.
In order to prevent the race conditions, one should ensure that only one process can access the shared data at a time. It is the main reason why we need to synchronize the processes. Another solution to avoid race condition is mutual exclusion.
go run -race . To detect it, the code should run in heavy load, and race conditions must be occurring. You can see the output of the race condition checker. It complains about unsynchronized data access.
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.
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.)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With