arc<mutex<T>> vs arc<refcell<T>>

When, if ever, does it make sense to use Arc<RefCell<T>> instead of Arc<Mutex<T>> ?

Not really, since the Arc is there to provide Sync access but RefCell is not Sync.

1 Like

Edit: For context of this first part of my answer: Of course Rc<RefCell<T>> is a very sensible (and not uncommon) type, and I assume you don't question that. So this is comparing Arc<RefCell<T>> not only with Arc<Mutex<T>> but also with Rc<RefCell<T>>.

The point of Arc when compared to Rc is that the former has atomic (and thus thread-safe) reference counting, which means that, while there's a very slight performance loss due to the need to atomic operations, it gains Send and Sync implementations. The precise bounds are

impl<T: ?Sized + Sync + Send> Send for Arc<T> {}
impl<T: ?Sized + Sync + Send> Sync for Arc<T> {}

(side-note: the reason why these require T: Send + Sync, not just Sync which is the condition for &T: Sync or &T: Send, is that a reference-count-1 Arc allows extracting the value with try_unwrap, or mutating the inner value with get_mut; and even if it didn't have those, the Drop implementation would have to be executed on a different thread anyways).

Now, RefCell vs. Mutex differ in panicking vs. locking, but more importantly in the fact that the latter is thread-safe, while the former isn't. RefCell not being thread-safe shows in its Sync implementation

impl<T: ?Sized> !Sync for RefCell<T> {}

Since RefCell<T> will never be Sync, this means that Arc<RefCell<T>> will never be Send or Sync, which in turn means, there's no advantage left over Rc<RefCell<T>>.


By the way, a related question is when Mutex<T> makes sense, compared to RefCell<T>, in situations where both could be used. They differ, as mentioned above, in locking vs. panicking and in their Sync implementation.

impl<T: ?Sized + Send> Sync for Mutex<T> {}

However this means that for a concrete known type T: !Send, a Mutex<T> is quite similar to a RefCell<T>, with identical Send/Sync implementations (i.e. they both implement neither). How does their behavior compare then? How useful is a Mutex if you don't use it between multiple threads?

A Mutex's locking operation only blocks if another lock is already held. But if the Mutex isn't Sync, then no other thread can be accessing the Mutex at the same time; this means that once .lock() blocks, it's also always automatically going to be a deadlock situation (for that particular thread). In this sense, RefCell<T> is like a non-Sync Mutex but with deadlock detection, so the panicking can be seen as an (almost) always advantageous behavior, at least, assuming that deadlocks are never intended behavior.

The last difference then is poisoning. But a Mutex that only a single thread accesses is hard to get poisoned anway. AFAICT, it's really only possible if you use catch_unwind, otherwise a panic that poisons the Mutex would also take down the whole thread, which is the only thread that has access to the Mutex in the first place.

Of course Mutex<T> with T: !Send could potentially make sense if it's part of generic code, where only parts of the API have a T: Send bounds, and other, still useful, parts don't (but still contain a Mutex).

14 Likes