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
UnsafeCell
s 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).
-
RefCell
would 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.Cell
deserves 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).
-
ReentrantMutex
would 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.
Sync
thus expresses the idea of being safe to use in parallel / "parallelism-safe";Send
is 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&mut
access count asSend
ing? I'd say that in English it doesn't, but in Rust's model it does), or worse, a tautological definition (somethingSend
is 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
ReentrantMutexGuard
only implementsDeref
/ does not implementDerefMut
(like aRwLockReadGuard
), thus only exposing:&'lock ReentrantMutex<T> -> &'lock T
The 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
&T
access may occur from within arbitrary threads, the "flock" of&T
references "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&mut
APIs / access: that is, it offers an unguarded&mut ReentrantMutex<T> -> &mut T
API.
- 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 T
to already be safe to cross thread boundaries. In other words, we have to have:RM<T> : Send => T : Send
It 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 beSync
as 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 theReentrantMutex
to 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 : Send
as a candidate? Well, indeed, if you think about what I mentioned above in this very post, I summarizedSend
as 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 + Sync
types, even if overlapping a marker trait ought to be fine. In the future, hopefully, themarker_trait_attr
feature 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 aSendOrSync
kind 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
Sync
thanks toReentrantMutex
beingSync
in 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>
↩︎