Reentrant Mutexes in Rust

Moving a conversation in a closed GitHub issue over here...

The problem is that Rust is so paranoid about data races, that one has to uses mutexes all over the place (if using shared memory) to fight the borrow checker off. This results then in increased risk of deadlocks due to the fact that locks are not reentrant.

@havelund parking_lot crate provides reentrant mutex if you need it:
https://amanieu.github.io/parking_lot/parking_lot/struct.ReentrantMutex.html

I‘m curious what’re the reasons behind not providing reentrant mutex as the default implementation in the standard library?

I think it's because Mutex and friends were originally meant to be wrappers around the native OS synchronization primitives, and some OS mutexes are not reentrant, though I'm not 100% sure on that.

I'd rather say that rentrant mutexes don't make a ton of sense in Rust most of the time.

The idiomatic Rust API for a mutext is to have a lock method which, given a &Mutex<T>, returns an &mut T (apropriately wrapped into a guard object). For reentrant mutex, this API doesn't work: you can't return &mut T because reentrancy allows one to get the second &mut T, violating aliasing guarantees. And fn lock(&Mutext<T>) -> &T signature is much less useful, as you naturally just have &T if you have Mutex<T>. I think it might makes sense when T: !Sync, because ReentrantMutex<T> maybe can be Sync (not sure about this).

2 Likes

Right, in parking_lot its Sync only requires T: Send, which lets you get interior mutability with inner types like ReentrantMutex<RefCell<T>>.

2 Likes

This. Rust &mut fights concurrency, not just parallelism. This means that re-entrancy can be just as bad as accessing from multiple threads (if we ignore !Sync).

If the control flow is "hidden" behind an abstraction layer (executor etc.) resulting in the re-entrancy being non-trivial, then the code should try_lock and handle the failure rather than locking and hoping there is no deadlock.

So the solution here is to rely on !Sync to make a difference, which is indeed well spotted. But then I fail to see how ReentrantMutex<RefCell<T>> is much different than RwLock<T> (two concurrent writers within the same thread deadlock / panic, don't they? I feel like I may be missing an obvious use case...). If it is to be used with !Sync and still avoid deadlocks / panics, then, imho, Cell ought to be used.

Indeed, ReentrantMutex<StructWithCellFields> seems to be the perfect fit for this:

  • ReentrantMutex allows to have StructWithCellFields be shared across threads,

  • the Cell fields allow reentrancy-safe mutations ("atomic" from the point of view of a single thread).

If you do see the point with Cell, then RefCell is just an extension for more complicated types. As long as you keep short-term borrows, it would be useful the same way. Use a scoped borrow_mut() for some mutation, then call something potentially reentrant, then borrow_mut() again for something else, etc. Plus the ReentrantMutex makes the whole operation work atomically at the outermost lock, as far as other threads are concerned.

1 Like

True, if one uses a RefCell as they would use Cell then all is fine :+1:

Still, regarding the OP, they were worried about the potential of there being a deadlock (or a panic may I add); and a single ReentrantMutex<Cell<_>> is guaranteed to be both panic and deadlock free. Plus, Cell has the mental semantics of C/C++ mutation, so it might be more ergonomic for them to work in terms of Cell rather than RefCell.

Reentrancy is just one form of deadlock. You can still have other issues, like an ABBA deadlock between two threads.

Well, ok, you said a single ReentrantMutex... You do have to make sure you don't do anything that would wait on other threads while you hold the lock, like a channel recv, where those other threads might need the lock too.