Understanding the reason for manually implementing the Sync trait

Hello, I was reading atomics and locks by Mara Bos and was a bit confused about something:
I wanted to understand that why we have added a bound T: Send while implementing the Sync auto/marker trait? what's the intuition behind it?

#![allow(unsafe_op_in_unsafe_fn)]

use std::cell::UnsafeCell;
use std::mem::MaybeUninit;
use std::sync::atomic::{
    AtomicBool,
    Ordering::{Acquire, Release},
};

pub struct Channel<T> {
    message: UnsafeCell<MaybeUninit<T>>,
    ready: AtomicBool,
}

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

impl<T> Channel<T> {
    pub const fn new() -> Self {
        Self {
            message: UnsafeCell::new(MaybeUninit::uninit()),
            ready: AtomicBool::new(false),
        }
    }

    /// Safety: Only call this once!
    pub unsafe fn send(&self, message: T) {
        (*self.message.get()).write(message);
        self.ready.store(true, Release);
    }

    pub fn is_ready(&self) -> bool {
        self.ready.load(Acquire)
    }

    /// Panics if no message is available yet.
    ///
    /// Tip: Use `is_ready` to check first.
    ///
    /// Safety: Only call this once!
    pub unsafe fn receive(&self) -> T {
        if !self.ready.load(Acquire) {
            panic!("no message available!");
        }
        (*self.message.get()).assume_init_read()
    }
}
  • Send means, values can travel from one thread to another.
  • Sync means, the type somehow orchestrate access from different tasks aka is immune to multi-threading. And if a value travels by this orchestration from one thread to another, it has obviously be Send.

If the Channel itself is Send, thread A can create a Channel and this channel can be moved to thread B.

When Channel is Sync, thread A can create a channel and hand out a (shared) reference to the channel to thread B. So the channel can be accessed simultaneously by different threads.

Because the Channel in this example is some sort of container where a value T can be stored in and taken out, this type T needs to be Send because either the whole channel can move between threads, or different threads access the channel simultaneously (by references).

So:

  • Send is for moves.
  • Sync is for references.
1 Like

If Channel is Sync, send and receive can be called from different threads thus moving a value of type T between threads. Which is only OK if T is Send.

Now, this could be a bound on the Channel itself. But that would preclude using Channel on types that don’t implement Send even within a single thread, which could be occasionally useful.

1 Like

Channel<T> being Sync and suppose if I am sending a reference to Channel<T> (as it is Sync) across thread boundaries then what we're technically doing here is sending T and hence the bound of T: Send, is my understanding correct?

You're sending T only if you access T via .receive() from a different thread after you have sent a reference of Channel<T> to the different thread.

The pure fact that Channel itself is accessed by different threads does not require the T stored inside to be Send as long as only the original thread that moved T inside accesses this T.

However, the whole purpose of this Channel is that a thread different from the sending one is receiving T via the .receive() method, so T needs to be Send.

1 Like

clear, thanks!