Why do we need the `Send` constraint here?

I'm reading the mara.nl synchronization guide. This excerpt is from the spinlock section:

use std::cell::UnsafeCell;

pub struct SpinLock<T> {
    locked: AtomicBool,
    value: UnsafeCell<T>,
}

As a precaution, UnsafeCell does not implement Sync, which means that our type is now no longer shareable between threads, making it rather useless. To fix that, we need to promise to the compiler that it is actually safe for our type to be shared between threads. However, since the lock can be used to send values of type T from one thread to another, we must limit this promise to types that are safe to send between threads. So, we (unsafely) implement Sync for SpinLock<T> for all T that implement Send, like this:

unsafe impl<T> Sync for SpinLock<T> where T: Send {}

How can the lock be used to send the wrapped value from one thread to another? It can only move the &mut T between threads, not the actual value, since the SpinLock itself hasn't been declared Send and it owns the wrapped value.

If you give me a &mut T, I can swap out the T value for another, moving the original (perhaps across threads).

2 Likes

Which by the way explains why Pin had to be added. Pin<&mut T> allows changing T, but forbids moving out of T (by using std::mem::swap for example).

Pin prevents moving in memory, but it does not prevent moving to another thread in the sense of Send. in particular Pin<&mut T> allows calling the destructor from another thread, which is unsound on most non-Send types

My comment was more about nature of &mut T. Pin<&mut T> relaxes what you can do with T. Of course I wouldn't recommend it as a general solution to preventing types from being Send.

Can you give an example of this? I cannot think of any way of dropping T without using unsafe, when all I have is Pin<&mut T>.

2 Likes

Thank you! I wasn't aware of this safe API. It goes to show how careful one has to be, when writing unsafe code.

1 Like