Edit: "merely", relatively to Rust :) on an absolute scale, still way better than C for concurrency.
Edit: "merely", relatively to Rust :) on an absolute scale, still way better than C for concurrency.
Often. Way too often. And it always break at 3 in the morning when you are hiking somewhere where there is no internet connection.
Besides, race conditions tend to break on the worst possible ways. It either silently corrupts data, or makes your system stop working but stay active, so the OS does not know it must launch a new instance.
You can drive nails with a knife by holding the nail and hitting it with the side of the knife. It takes work to do right with you occasionally cutting yourself. Some people will even loose a hand.
You can use a hammer. You will get the nails in smoother with less rework required. You occasionally hurt yourself but not as bad as a knife. You can also smash the nerves to point you don't feel anything.
You can use a nailgun with safety switch. It's ultra-fast and can't hurt you if you're aiming where the nails go. It cost a bit more. It can hurt you if you turn the safety off or aim it at yourself. Simpler than mental effort into using knives or hammers safety. Less tiring, too. Some people even think they're fun.
That's C, C++, and languages that are memory-safe respectively. Rust's advantage of no GC is like a nail-gun that costs same as a hammer. It has the benefits but not the primary disadvantage. If you're a craftsman, you're time and energy are valuable. Use the best tools you can for whatever work you're doing. That is, what gets it done quickly, done right, and easy to fix later w/ smaller problems.
Rookie mistake (and a shoot-yourself-in-the-foot-at-2-am) mistake coming up:
package main
import ( "fmt" "sync" )
func main() { var wg sync.WaitGroup
for i := 0; i < 10; i++ { wg.Add(1)
go func() {
fmt.Printf("i= %d\n", i)
wg.Done()
}()
}
wg.Wait()
}https://play.golang.org/p/XDzFq_XK_1
And what about this code:
package main
import "fmt"
func main() {
var intArray []int for i := 0; i < 1000; i++ {
intArray = append(intArray, i)
fmt.Printf("i= %d\n", i)
}
fmt.Println(intArray)
}https://play.golang.org/p/UuI4uESZ_f
If you don't care if intArray is in the proper order, you might do something like this:
package main
import ( "fmt" "sync" )
func main() { var wg sync.WaitGroup var intArray []int
for i := 0; i < 1000; i++ { wg.Add(1)
go func(i int) {
intArray = append(intArray, i)
fmt.Printf("i= %d\n", i)
wg.Done()
}(i)
}
wg.Wait()
fmt.Println(intArray)
}What's wrong?
On my multi-core computer (though not on playground), len(intArray) could be as low as 500!
Why?
Because a = append isn't atomic.
Go, which is so pedantic about "stupid" mistakes (including something, changing your mind, and not "unincluding" it), didn't catch this. Not an error and no warning.
But the worst part is that when you loop until 10, it works. You can unit test it, integrate test it, and have it break at any point in time.
They also have distributed programming course lecture notes that are open to the public with golang specific info on how to deal with race conditions.
You'll have the same experience in Rust if you research why they've done certain things, but can quickly get lost on the mailing list unless you've read books like EoPL http://www.eopl3.com/
It can basically corrupt the whole program.
I bet half go apps out there have data race. People who boast about Go's simplicity can't even see the elephant in the room. A simplistic type system doesn't fix unsafe concurrency. You'd think that safe concurrency would be an important design goal for a highly concurrent language, well apparently it isn't.
My standard advice on concurrency is, quite simply, don't. It always looks way simpler than it actually is. If you're doing something that _requires_ it, get all the help you can. Type-level enforcement sounds _excellent_ to me.
But there are definitely use cases where you don't want to use actors, where you might want to compose several futures together without the overhead of actor mailboxes.
Erlang's concurrency is "don't communicate by sharing, share by communicating" enforced at the language level: an Erlang system is composed of processes which each have their own heap (and stack) and an incoming "mailbox", an Erlang process can only interact with the world by calling BIFs (built-in "native" functions, Erlang syscalls if you will) or sending messages to other processes, and messages can only contain immutable data structures (mutation happens only at the process level).
Of course one of the sources for this design is that Erlang comes from a world where 1 = 0 (if you don't have redundancy you don't have a system) thus two processes may live on different nodes (erlang VMs) on different physical machines and shouldn't behave any differently than if they were on the same node.
And pattern matching in Erlang is a joy every developer should experience. I've not been impressed with my limited exposure to Scala, which also bears the burden of trying to be a kitchen sink language.
The crux with such systems is you tend to rely on them...
I'll answer given my own experience :
At my former company, we had a websocket-based service that allowed symetric communication between the clients. We had around 10% connection failures, and we thought it was causes by websocket well-known incompatibilities with some network stack, and we had a fallback to ajax polling.
Several month later, someone touched this part of the code and found a data race. After we fixed it, the failure rate dropped to 5%.
Is 5% of failure big ? It depends on your business. In our case it wasn't too bad because we had a backup plan. But if every customer have a 5% chance of application breakage each time he connects, you may have trouble building a big user-base.