A better term than "Thread safe"?

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).
  • 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 :sweat_smile:):


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" :slightly_smiling_face:.

  • 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 as Sending? I'd say that in English it doesn't, but in Rust's model it does), or worse, a tautological definition (something Send is safe to send…).

Bonus: Exercise

What is the correct impl to express the thread-safety of the aforementioned ReentrantMutex?

Click to expand

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), which ReentrantMutex<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 for RM<T>[4] not to be Sync 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 the ReentrantMutex to begin with! This impl is basically sound because when T : 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 summarized Send 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…> or ReentrantMutex<Cell…> to become Sync, when the non-wrapped variants weren't :slight_smile:

    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, the marker_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 a SendOrSync 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 to ReentrantMutex being Sync 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.


  1. 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. ↩︎

  2. 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 :ear:s! ↩︎

  3. shorthand for ReentrantMutex<T> ↩︎

  4. shorthand for ReentrantMutex<T> ↩︎

15 Likes