Which as the article points out means you pay the overhead for the thread safety every time you use the object wether you need to or not.
So it's a design tradeoff, and I can make the argument for either case. However in my opinion Rust made the wrong choice. They chose the "you don't have to think of it" case, which I think is wrong for a systems programming language.
If I want to make a bunch of changes to an object atomically (or a bunch of bookkeeping to multiple objects) I still need a manual lock (mutex), and then the internal bookkeeping is deadweight. I also might use an object on a single thread for a while and then hand it off to another thread which then has ownership (so still requires no locking). Or even hand it off to a group of threads which then need to use it in a safe way, presumably by calling different functions.
I don't mean to imply there is One True Way or that Rust Got It Wrong.* This is merely one choice of many when exploring the design/functionality space.
* OK on mixed case identifiers they did :-)
Your average object that contains a bunch of mutable fields will, even in Rust, be expressed using a mutex. For instance, here's a very simple race-free program that transfers $20 to Alice from Bob and $30 from Bob to Alice: https://play.rust-lang.org/?version=stable&mode=debug&editio... It uses
struct Bank {
alice_balance: i32,
bob_balance: i32,
}
and keeps the bank inside a Mutex (or more specifically inside an Arc<Mutex<Bank>> to manage ownership across threads). This is a pretty straightforward pattern based on the Mutex docs: https://doc.rust-lang.org/std/sync/struct.Mutex.htmlIn this case, the individual threads that perform the transfer are not applying any additional overhead to alice_balance or bob_balance. Those are regular old int32_t variables in a regular old struct, same as C/C++. The only thing that's being prevented is that the compiler won't let me write a version of this program that doesn't have the Mutex. There's a lot of analysis going on inside the compiler to confirm this, but once the compiler is happy, there's no runtime overhead compared to the equivalent C/C++ program.
(So I don't think "you don't have to think of it" is really what's going on. You have to think of it. It's just that if you don't, your program won't compile.)
Now, you can write a version of the program that uses thread-safe data types for the balances directly, maybe something like this: https://play.rust-lang.org/?version=stable&mode=debug&editio...
struct Bank {
alice_balance: AtomicI32,
bob_balance: AtomicI32,
}
And it is true that there is now extra overhead in this version of the program, and the "advantage" is you could get rid of the mutex and it would still compile. But why would you write it this way? You know that you want the accesses to be transactional. You know you're going to want a mutex (or something) around the Bank. Why add the atomic types?If Rust somehow used atomic integers by default (or something like a global interpreter lock) so you didn't have to think about it and code compiled whether or not you locked things properly, then yes, I'd agree. But it doesn't - it uses normal integers and things fail to compile.
As someone who has programmed in C++ for over 10 years and in Rust for over 5 years I have to hard disagree on this one. (:
Rust made precisely the right choice because by default in gets rid of all of the mental overhead of "is this actually safe to call/move this across multiple threads?" while giving you the tools to bypass it if you actually want to do it the C++ way, and then you have to think about it. (But only if you explicitly choose to!)
> If I want to make a bunch of changes to an object atomically (or a bunch of bookkeeping to multiple objects) I still need a manual lock (mutex), and then the internal bookkeeping is deadweight.
Then just... don't add any internal bookkeeping to your type? (:
Or, depending on the kind of bookkeeping you use, you might not have to pay for it anyway. For example, Rust has atomic integer types which require you to access them in a safe way, however, they also have this method on them:
pub fn get_mut(&mut self) -> &mut u32
...which means that if you wrap that in an outer Mutex you will have an &mut to it, which means that even though the type is thread-safe by default you do not have to pay anything extra to use it if you decide to put it in an outer Mutex and do the synchronization yourself. The overhead of its "built-in" synchronization is then completely gone!
Isn't this the best of both worlds? (:
> I also might use an object on a single thread for a while and then hand it off to another thread which then has ownership (so still requires no locking).
...so you'll have a &mut reference on that single thread, in which case no locking is required. Am I missing something?
> Or even hand it off to a group of threads which then need to use it in a safe way, presumably by calling different functions.
Fair enough. But in that case you can just use an UnsafeCell and do it the C++ way if you really want to.
That’s not correct. See, eg, `Mutex::get_mut` (https://doc.rust-lang.org/stable/std/sync/struct.Mutex.html#...). It gives access to the underlying data without any locks or synchronization, if you can prove that no other threads ca access the mutex concurrently at the point of call.
That’s not entirely true, in the sense that you can have non-threadsafe functions operating on thread-safe types e.g. Mutex::get_mut.
I didn’t say it was unsafe.
However, for Rust functions there's no hole, because Rust doesn't allow thread-unsafe global state. All globals always have to be immutable or use some synchronization primitive. Therefore, a safe Rust version of the `setlocale()` function would be forced to use atomics or mutexes to set the locale without data races. It's still a bad API, and Rust can't fix that.