Remix.run Logo
jstimpfle 12 hours ago

Given a data item of non-thread safe type (i.e. not Mutex<T> etc), the borrow checker checks that there's only ever one mutable reference to it. This doesn't solve concurrency as it prevents multiple threads from even having the ability to access that data.

Mutex is for where you have that ability, and ensures at runtime that accesses get serialized.

dwattttt 11 hours ago | parent [-]

The maybe unexpected point is that if you know you're the only one who has a reference to a Mutex (i.e. you have a &mut), you don't need to bother lock it; if no one else knows about the Mutex, there's no one else who could lock it. It comes up when you're setting things up and haven't shared the Mutex yet.

This means no atomic operations or syscalls or what have you.

jstimpfle 11 hours ago | parent [-]

Do you have an example? I don't program in Rust, but I imagine I'd rarely get into that situation. Either my variable is a local (in a function) in which case I can tell pretty easily whether I'm the only one accessing it. Or, the data is linked globally in a data structure and the only way to access it safely is by knowing exactly what you're doing and what the other threads are doing. How is Rust going to help here? I imagine it's only making the optimal thing harder to achieve.

I can see that there are some cases where you have heap-data that is only visible in the current thread, and the borrow checker might be able to see that. But I can imagine that there are at least as many cases where it would only get in the way and probably nudge me towards unnecessary ceremony, including run-time overhead.

dwattttt an hour ago | parent | next [-]

It's relevant when you have more complex objects, such as ones that contain independent mutexes that lock different sections of data.

You want the object to present its valid operations, but the object could also be constructed in single or multithreaded situations.

So you'd offer two APIs; one which requires a shared reference, and internally locks, and a second which requires a mutable reference, but does no locking.

Internally the shared reference API would just lock the required mutexes, then forward to the mutable reference API.

adwn 11 hours ago | parent | prev | next [-]

When you construct an object containing a mutex, you have exclusive access to it, so you can initialize it without locking the mutex. When you're done, you publish/share the object, thereby losing exclusive access.

    struct Entry {
        msg: Mutex<String>,
    }
    ...
    // Construct a new object on the stack:
    let mut object = Entry { msg: Mutex::new(String::new()) };
    // Exclusive access, so no locking needed here:
    let mutable_msg = object.msg.get_mut();
    format_message(mutable_msg, ...);
    ...
    // Publish the object by moving it somewhere else, possibly on the heap:
    global_data.add_entry(object);
    // From now on, accessing the msg field would require locking the mutex
jstimpfle 7 hours ago | parent [-]

Initialization is always special. A mutex can't protect that which doesn't exist yet. The right way to initialize your object would be to construct the message first, then construct the composite type that combines the message with a mutex. This doesn't require locking a mutex, even without any borrow checker or other cleverness.

adwn 7 hours ago | parent [-]

Dude, it's a simplified example, of course you can poke holes into it. Here, let me help you fill in the gaps:

    let mut object = prepare_generic_entry(general_settings);
    let mutable_msg = object.msg.get_mut();
    do_specific_message_modification(mutable_msg, special_settings);
The point is, that there are situations where you have exclusive access to a mutex, and in those situations you can safely access the protected data without having to lock the mutex.
jstimpfle 7 hours ago | parent [-]

Sorry, I don't find that convincing but rather construed. This still seems like "constructor" type code, so the final object is not ready and locking should not happen before all the protected fields are constructed.

There may be other situations where you have an object in a specific state that makes it effectively owned by a thread, which might make it possible to forgo locking it. These are all very ad-hoc situations, most of them would surely be very hard to model using the borrow checker, and avoiding a lock would most likely not be worth the hassle anyway.

Not sure how this can help me reduce complexity or improve performance of my software.

imtringued 10 hours ago | parent | prev [-]

>I don't program in Rust, but I imagine I'd rarely get into that situation.

Are you sure? Isn't having data be local to a thread the most common situation, with data sharing being the exception?

>Or, the data is linked globally in a data structure and the only way to access it safely is by knowing exactly what you're doing and what the other threads are doing.

That's exactly what the borrow checker does. It tracks how many mutable references you have to your data structure at compile time. This means you can be sure what is local and what is shared.

Meanwhile without the borrow checker you always have to assume there is a remote probability that your mental model is wrong and that everything goes wrong anyways. That's mentally exhausting. If something goes wrong, it is better to only have to check the places where you know things can go wrong, rather than the entire code base.

jstimpfle 8 hours ago | parent [-]

I use lots of locals but only to make my code very "local", i.e. fine-grained, editable and clear, using lots of temporary variable. No complicated expressions. That's all immutable data (after initialization). I rarely take the address of such data but make lots of copies. If I take its address, then as an immutable pointer, maybe not in the type system but at least in spirit.

I keep very little state on the stack -- mostly implicit stuff like mutex lock / mutex unlock. By "state" I mean object type things that get mutated or that need cleanup. I always have a "database schema" of my global state in mind. I define lots of explicit struct types instead of hiding state as locals in functions. I've found this approach of minimizing local state to be the right pattern because it enables composability. I'm now free to factor functionality into separate functions. I can much more freely change and improve control flow. With this approach it's quite rare that I produce bugs while refactoring.

So yes, I have lots of locals but I share basically none of them with other threads. Also, I avoid writing any code that blocks on other threads (other than maybe locking a mutex), so there's another reason why I would not intentionally share a local with another thread. Anything that will be shared with another thread should be allocated on the heap just for the reason that we want to avoid blocking on other threads.

In that sense, the borrow checker is a tool that would allow me to write code more easily that I never wanted written in the first place.