Why we implement Send trait for Mutex?

I am implementing my own spinlock currently.
I unstander we have to implement Sync trait for Mutex in order to use it across different threads.
Just like this unsafe impl<T: ?Sized + Send> Sync for Mutex<T> {}

But I don't understand why we need Send trait for Mutex,
like unsafe impl<T: ?Sized + Send> Send for Mutex<T> {}

Here is a part of the implementation of std::sync::Mutex.
https://doc.rust-lang.org/src/std/sync/mutex.rs.html#125

Could anyone help me figure this out?
Thanks in advance!

P.S. Send trait is needed for Mutex, if we want to wrap it in Arc and share it between multiple threads.
https://play.rust-lang.org/?version=nightly&mode=debug&edition=2018&gist=1d2e8fb30dbc8d67346be1ee18affaf6

Sometimes you might want to pass ownership of the mutex to another thread. Maybe you'll create it in one thread, do some stuff, then pass it to another thread or put it in an Arc to do the rest of the task?

Another reason is simply because we can. By making as many types Send as possible (assuming it is sound, of course), you are less likely to encounter situations where your own type can't be Send because it contains a Mutex somewhere deep down.

2 Likes

Thanks for you explanation!
You make my understanding better now.

Objects need to impl Send to be placed in a static, which is a pretty common use case for Mutex.

Actually statics only require Sync.

  • and quite astonishingly, static muts do not require Send either :astonished: :fearful:
1 Like

Disclaimer: this is just an idea of the top of my head.

If Mutex would only have Sync, sure it would be possible to send &Mutex to another thread and use it for thread synchronization. But... a reference has a lifetime, so coordinating that across threads is pretty complicated. I'm not even sure it's possible, as thread::spawn requires a 'static closure.

That means it would only really be possible to do thread sync with a Mutex over a global variable, not so nice.

With Send, we can clone Arc<Mutex<T>> and happily send that to another thread.

2 Likes

I also find that the std library implements both Sync and Send trait for Arc.
https://doc.rust-lang.org/src/alloc/sync.rs.html#201-204
I thought that we are not going to share references of Arc between threads, and each thread has their own Arc which points to the same memory location.
So why is Sync trait needed for Arc?

See codes below, which may implies that Send is sufficient.
https://play.rust-lang.org/?version=nightly&mode=debug&edition=2018&gist=53ef1d86f47f9f6c3bb0b9642bb079d8

In theory. In theory our types are flat and have no nested Arcs.

In practice, I may end up with something like this:

struct MyStruct {
    must_sync: Arc<Mutex<usize>>,
    can_be_shared_read: String,
}
let shared = Arc::new(RwLock::new(MyStruct::new()));
for _ in 0..10 {
    thread::spawn(|| {
        let shared_data = shared
            .read()
            .unwrap();
        //use other data in shared_data
        ...
        shared_data
            .field
            .lock()
            .unwrap() += 1;
    };
}

Allowing as many things to be Sync and Send allows the programmer to choose how to design their structure, without being needlessly bounded by the threading rules.

As for why it's safe, Arc uses atomics (Hence, the name stands for "A tomically R eference C ounted"), which are safe to use from multiple threads at once.

5 Likes

Thanks, I understand why we need Send trait now.

I'm wondering about this too though. Sure Sync would let you put it in a global and access it from several threads, but is it even possible to send a non-'static reference to a different thread?

Yes, namely with crossbeam scoped thread and most notably with rayon.

2 Likes

There are mainly three ways of sending an Arc "shared reference" (in its broader sense) to another thread:

  • nested Arcs, as @OptimisticPeach said (an Arc is a form of "shared reference");

  • scoped threads, as @Hyeonu mentioned (similar to nested Arcs, when complex structs are involved, the inner Arc may not be essential but it leads to simpler code rather than refactoring everything to optimize away the inner Arc);

  • And more simply, the "troll" thing that many people forget about, but which is not allowed to cause memory unsafety, is that a Rust (shared) reference & _ can very well be 'static (more generally, arbitrarily big / unbounded), by heap-allocating it and ensuring it is never freed: Box::leak.

    So you can get a &'static Arc<_> that you can then Copy and Send to another thread with good ol' thread::spawn, by doing: &*Box::leak(Box::new(arc))

Aside: recap of the `T` bounds for `Arc<T>` to be `Send, Sync`

Arc<T> is a hybrid between &T (shared reference to a T) and a Box<T> (pointer owning a T).

So, for an Arc<T> to be safe to Send across threads, it conservativley needs to meet the Send-safety requirements of these two other types:

  • &T : Send where T : Sync, hence the T : Sync bound;

    • (In other words: given that an Arc may have a clone in the current thread before being sent to another one, multiple threads get shared access to the T thanks to the Deref impl, so if the pointee is mutable through a shared reference (Aliased / Shared / Interior Mutability), then such mutation needs to be be Synchronized to prevent the unsoundess of a data race)
  • Box<T> : Send where T : Send, hence the T : Send bound.

    • In other words, &mut-based APIs, such as Drop's, may be triggered on the pointee from a different thread that the one where it was created; so doing can only cause unsoundness if the type T is not Send.
  • these bounds suffice, since the only other thing behind Arc's implementation are the counters, which are atomic integers which are thus always Send + Sync (and this is the difference with Rc : whilst Rc does meet the first two points regarding T, since the counters are mutated in a non-thread-safe fashion (Cell<usize>s), Rc<T> is neither Send nor Sync).

And given the existence of Arc::clone(&'_ Arc<T>) -> Arc<T>, which allows to create an owned version of this very T from a shared reference, for Arc<T> to be Sync, it must already be Send, hence these very same bounds; and it turns out this is sufficient.

That is,

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

can be seen as a (sufficient) consequence of:

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

(or the other way around, however you prefer to see it)

3 Likes

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.