"Buried" vs. "externalized" interior mutability

Hi,

I was wondering if there are common cases in which one prefers burying the interior mutability inside the struct (making everything accept &Self and wrapping the fields with RefCell or Mutex as needed) vs. externalizing it (making everything accept &mut Self and letting the caller use Rc<RefCell> or Arc<Mutex>).

In fact I am also wondering if there are accepted terms for the two conventions...

1 Like

Let me hook into this question with another thats (I think) similar: What about Arc?
Is this

struct Foo(something);

impl Foo {
    fn new() -> Arc<Self> {
        Arc::new(Self(something))
    }
}

preferred or this

 struct Foo(Arc<something>);

impl Foo {
    fn new() -> Self {
        Self(Arc::new(something))
    }
}

Assuming Foo is only meant to be used in contexts where an Arc is needed

2 Likes

Firstly I think this depends on the use case.
If you have some kind of shared state where you need to access multiple fields on most operations, I'd prefer an Arc<Mutex<SharedState>> (or use an RwLock if you also have significant read-only access).
On the other hand, if the shared state is just a container of otherwise unrelated shared fields, which are accessed independently, you could also move the internal mutability inside the shared state struct.

struct SharedState {
    counter: Atomic<u8>,
    frobnicator: RwLock<Frobnicator>,
}

let shared_state = Arc::new(SharedState { .. }):

What I would not do, is to move the Arcs inside such a struct, since you will unnecessarily increment and decrement all ref counts at the same rate for any field anyways.

A special case where it would make sense to move the Arcs inside the struct is, if you have a collected struct of shared state at one central location and other threads only use parts of the data within that struct. Then it would be beneficial to have the option to share the individual fields across threads.

Also I'd avoid a signature like fn new() -> Arc<Self> since it is imho unexpected for new() to return anything other than Self.

2 Likes

By the way, thanks for these questions. It makes you think about stuff, you did not think about before and gives you opportunities for improvement:

3 Likes

Firstly I think this depends on the use case.

Yes, I worded the question in expectation that the answer was "it depends".

I am asking because I noticed in the OpenVMM sources that they write their devices as always &mut, assuming that they are managed as Arc<Mutex<dyn ChipsetDevice>> (link). In a similar project instead I am placing only the register state in the mutex, burying the mutex inside the struct. In my case I am doing it to provide a clean separation between the configuration data (which is outside the interior-mutable part) and the runtime state, but it's true that you end up passing around a lot of MutexGuards or RefMuts.

The big advantage of keeping the interior mutability private is that you can reduce the possible misuses.

For example, if you have a Mutex, then there is the possibility of deadlock. If the Mutex is private, then you can examine all the functions that lock it, verify that none of them use any other locks, and be confident that this type won't be responsible for a deadlock.

Or, if you have an atomic counter, then then you can be sure it only counts up and is not reset if you keep it private.

Basically all the reasons for encapsulation may apply here. If you make your interior mutability public, then you are giving up all ability to control how the contents are mutated. (Of course, that's fine if there are no such rules worth enforcing.)

6 Likes