3 ms·
Another difference is that in C/C++ thread-safety documentation usually talks about functions being thread-safe or not. OTOH in Rust, the thread-safety informat
by pornel 5y ago
Another difference is that in C/C++ thread-safety documentation usually talks about functions being thread-safe or not. OTOH in Rust, the thread-safety information is attached to data types, not functions. Even closure types like `Fn() + Sync + Send` are not about the function itself, but the data it is allowed to capture. So in a sense, you're allowed to call any function anywhere you want, but Rust will stop you from giving it data that it couldn't safely touch.
- haberman 5y agoWhich documentation do you mean? The Abseil docs I linked to (https://abseil.io/blog/20180531-regular-types#data-races-and-thread-safety-properties https://abseil.io/blog/20180531-regular-types#data-races-and...) are talking about thead-safety of types, not functions.
- gumby 5y ago> OTOH in Rust, the thread-safety information is attached to data types, not functions. 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 :-)
- geofft 5y agoMost of this internal bookkeeping is at compile time, right? 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&edition=2021&gist=c6f47a6102afaf31a90b21e28cd43df4 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.html https://doc.rust-lang.org/std/sync/struct.Mutex.html In 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&edition=2021&gist=9088c70ebfcae584de87bf38caa97b4d 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.
- kouteiheika 5y ago> 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. 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.
- matklad 5y ago> you pay the overhead for the thread safety every time you use the object wether you need to or not. That’s not correct. See, eg, `Mutex::get_mut` (https://doc.rust-lang.org/stable/std/sync/struct.Mutex.html#method.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.
- apatheticonion 5y agoAs someone new to Rust I appreciate the way you have articulated this.
- ridiculous_fish 5y agoThis seems rather like a hole in the scheme. There really are thread-unsafe functions like `setlocale` or `setupterm`, and the corresponding Rust crates allow you to use these functions from arbitrary threads. It's easy to produce a SIGSEGV by just invoking `ncurses::initscr()` a bunch in separate threads, without writing any unsafe code.
- firethief 5y agoThat's only a problem if you use code that is unsound. Well-written crates do not incorrectly mark functions safe.
- pornel 5y agoThread-unsafe C functions are a problem for Rust indeed. For example, C `setenv()` is unfixably broken for multi-threaded programs, and C code invoking it can crash Rust programs. Currently it's an open question what Rust should do about that. 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.
- masklinn 5y ago> OTOH in Rust, the thread-safety information is attached to data types, not functions. 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.