Multithreading errors are the worst to debug. In this case it's dead simple to identify at design time and warning flags should have gone up as soon as he started thinking about using any of the normal containers in a multithreaded environment.
We have almost one '9' of uptime!
int balance = 0;
int get {...}
void set(int balance) {...}
void withdraw(int amt) {
if amnt <= balance {
balance -= amnt;
}
}
it will be flagged immediately in code review as a race condition and/or something that doesn't guarantee its implied balance>=0 invariant. Because threading.But lift the logic up into a REST controller like pretty much all web app backends:
GET /balance/get
POST /balance/set
POST /balance/withdraw
And it will sail straight through review. (Because we pretend each caller isn't its own Thread.)There's simply no straightforward default approach that won't have you running into and thinking through the most esoteric sounding problems. I guess that's half the fun!
Hopefully someone will invent something like STM [1] in the distant year of 2007 or so [2]. It has actual thread-safe data structures. Not just the current choice between wrong-answer-if-you-dont-lock and insane-crashing-if-you-dont-lock.
[1] https://www.adit.io/posts/2013-05-15-Locks,-Actors,-And-STM-...
A task is an abstraction over those primatives in any language. To my knowledge TBB task graph abstract over a threadpool using exactly that concept.
From what I've seen swift is the only language that properly handles concurrency. I'm taking another crack at rust but the fact that everyone uses tokio for anything parallel makes me feel like the language doesn't have great support for concurrency, it just has decent typing which isn't a surpise to anyone.
(Well, go is not even memory safe under data races!)
Also, Java is one of the languages where you can just add `synchronized` as part of the method signature, and while this definitely doesn't solve the problem, I don't think "divorced from the data" is accurate.
I think the primary issue here is that synchronized should be the default case, with optionally lifting that very strict restriction for multi-threaded access.
Nonetheless, objects with methods that would do STM on their inner state would be a pretty cool design.
I've written highly concurrent software with bog-standard hash maps plus channels. There are so many advantages to this style, such as events being linearized (and thus being easy to test against, log, etc).
If only it were so easy.
Optimistic concurrency in general is a useful design pattern in many cases, though.
In other words, use Rust.