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).
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.
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.