I'd emphasize the "not always" part and say that "reentrant" is very much not a synonym of parallelism, but a super-set of it!
Rust expresses concurrency, in a general form, as shared / & access.
It expresses parallelism by adding Sync into a concurrency context.
So, there exist things that are &-safe / shared-safe / concurrent-safe, whilst not necessarily being Sync, since not parallelism-safe. In that case, such things would be re-entrant safe, but very much not thread safe.
- Anything immutable-when-shared / without shared-mutability / without
UnsafeCells somewhere deep inside it (or raw pointer indirection) is necessarily[1] generally concurrency-safe / reentrant-safe already (and minor weird APIs, this ought to lead to being parallelism-safe as well).
-
RefCellwould be the most canonical thing with shared mutability that remains reentrant-safe while not being parallelism-safe. Indeed, thanks to its runtime guard, it can detect re-entrant misusage.Celldeserves a special mention. While allowing concurrent mutations, it may be one of the few instances of such that would be guaranteed never to feature re-entrancy, by virtue of never lending inner references (hence featuring "register semantics", we could say).
-
ReentrantMutexwould be a wrapper type that is thread-safe by giving an illusion of parallelism (&/ concurrent access from within multiple threads is safe by virtue of blocking it at runtime, should it ever happen) even if the inner wrapper isn't really parallelism-safe.
All that to say that Rust does precisely this great job at decoupling some of these notions that have been, historically, quite tangled for a while; and for those used to that environment with everything muddied, it can be a bit hard to take a step back and rethink these distinctions that Rust makes.
In this instance, that re-entrancy is a main "danger" of concurrency, but that parallelism is not the only form of concurrency. Even more so, I am personally used to talking of re-entrancy as an opposition to parallelism, as in "single-threaded re-entrancy".
Here is a diagram showcasing some of the things I mean (I apologize for the colors and the font; I didn't want to spend too much time making it so I just went for one of the very first[2] available online tools out there
):
Now, regarding the naming, I like to say that something is "thread-safe" if all of its API is, which, as the diagram showcases, very often involves being Send and Sync when both shared and exclusive APIs are showcased.
Syncthus expresses the idea of being safe to use in parallel / "parallelism-safe";Sendis actually quite hard to put into words in a summarized fashion, but the gist of it would be about being safe to use across multiple threads sequentially / in a non-parallel fashion.
All in all, Rust's official terminology for Sync is quite neat: "safe to share across threads"
.
- The official terminology for
Send, however, is not as good: "safe to send across threads". What does send mean? It's either a quite informal definition (does&mutaccess count asSending? I'd say that in English it doesn't, but in Rust's model it does), or worse, a tautological definition (somethingSendis safe to send…).
Bonus: Exercise
What is the correct impl to express the thread-safety of the aforementioned ReentrantMutex?
Click to expand
-
Functionality: it's like a mutex, except the
ReentrantMutexGuardonly implementsDeref/ does not implementDerefMut(like aRwLockReadGuard), thus only exposing:&'lock ReentrantMutex<T> -> &'lock TThe key aspect is that this API will block the current thread as long as some other thread has gotten ahold of the lock. This means that whilst
&Taccess may occur from within arbitrary threads, the "flock" of&Treferences "flies together", one thread at a time. So the accesses from within multiple threads are guaranteed to occur, if ever, sequentially.- Also, a
ReentrantMutex<T>does not care about&mutAPIs / access: that is, it offers an unguarded&mut ReentrantMutex<T> -> &mut TAPI.
- Also, a
So, what should Bounds… and ThreadSafetyMarker stand for in the following impl:
unsafe
impl<T> ThreadSafetyMarker for ReentrantMutex<T>
where
T : Bounds…,
{}
-
When
ThreadSafetyMarker = Send?-
Bounds…=Send? -
Bounds…=Sync?
Answer
Given the aforementioned
&mut RM<T> -> &mut T, for&mut RM<T>[3] to be safe to cross thread boundaries, it is necessary for&mut Tto already be safe to cross thread boundaries. In other words, we have to have:RM<T> : Send => T : SendIt turns out the other direction holds as well: the only time it doesn't is when shared ownership enters the equation (e.g.,
Arc), whichReentrantMutex<T>has nothing to do with.So
RM<T> : Send <=> T : Send, i.e.,unsafe impl<T> Send for ReentrantMutex<T> where T : Send, {} -
-
When
ThreadSafetyMarker = Sync?-
Bounds…=Send? -
Bounds…=Sync?
Answer
This one is quite subtle. We already have kind of a tautological answer:
-
if
T : Sync, then there is literally no reason forRM<T>[4] not to beSyncas well. Indeed, "sequential shared accesses from within multiple threads" is a subset / more strict than "shared accesses from within multiple threads". If the latter is safe (Sync), then the former must be safe as well (RM<T> : Sync(because the locking method is&-based))./// It would be sound to have this unsafe impl<T> Sync for ReentrantMutex<T> where T : Sync, {}That being said, it's kind of a quite useless property, then: when
T : Sync, we didn't really need theReentrantMutexto begin with! This impl is basically sound because whenT : Sync,RM<T>is useless, and thus, harmless. And there isn't much point in a useless API, is there? -
so this kind of leaves
T : Sendas a candidate? Well, indeed, if you think about what I mentioned above in this very post, I summarizedSendas the property of sequential multi-threaded accesses being safe.
That is, "sequential shared accesses from within multiple threads" is also a subset / more strict that "sequential exclusive accesses from within multiple threads" (since we can always loosen an exclusive access (&mut) down to a shared one (&)).So this gives us:
/// More useful impl unsafe impl<T> Sync for ReentrantMutex<T> where T : Send, {}This is now genuinely useful, since it makes something such as
ReentrantMutex<RefCell…>orReentrantMutex<Cell…>to becomeSync, when the non-wrapped variants weren't
Now, currently, the coherence checker will prevent having both impls, since they would overlap for
Send + Synctypes, even if overlapping a marker trait ought to be fine. In the future, hopefully, themarker_trait_attrfeature will be stabilized, precisely to express that certain traits are guaranteed not to have contents / to only act as a marker, thereby allowing impls overlaps. With it, we could have aSendOrSynckind of bound.So the final correct answer, in future Rust, would be:
-
Bounds… = Send or Sync:unsafe impl<T> Sync for ReentrantMutex<T> where T : Send or Sync, {}
-
This exercise is relevant to my original remark, since when considering ReentrantMutex<RefCell<T>>, we have:
-
the type is
Syncthanks toReentrantMutexbeingSyncin this case, i.e., safe to share across threads / parallel access to the mutex can be attempted; -
the type is also re-entrant safe, since
RefCell<T>features a runtime check to guard against it, even if it features Shared Mutability. -
and yet the type involves no actual parallelism at runtime: the very design of
ReentrantMutex's locking logic prevents it! So it is safe to expose to parallel acesses (attempts), since the mutex is able to guard against those, (b)locking the extra threads so that exactly one gets actual access to the inner value.
unless it relies on mutating global storage: it wouldn't then technically have a reference / pointer to such mutated global storage, but conceptually it would, so my point still applies on the conceptual level. ↩︎
if anyone knows of an almost as easy-to-use as Free Venn Diagram Generator, but nicer-looking / a bit more customizable, I'm all
s! ↩︎shorthand for
ReentrantMutex<T>↩︎shorthand for
ReentrantMutex<T>↩︎
