Sync trait requirement in thread::spawn()


#1

The Book says that you may send to another thread only data that implement Sync. E.g., the following code doesn’t compile:

let mut data = Rc::new(vec![1, 2, 3]);

for i in 0..3 {
    let data_ref = data.clone();
    thread::spawn(move || {
        println!("{}", data_ref[0]);
    });
}

However, the declaration of thread::spawn() mentions no Sync:

pub fn spawn<F, T>(f: F) -> JoinHandle<T>
where F: FnOnce() -> T, F: Send + 'static, T: Send + 'static

Then how does rustc know that the parameter to spawn() must implement Sync?


#2

No, to send data you need it to implement Send. Rcs do not implement Send (use Arcs for that), so you’re getting an error. And you can clearly see this requirement from the thread::spawn() signature above: it requires the closure type and its return type to be Send, so that it may send the closure to the new thread and then get its result back.

Sync [mostly?] isn’t used directly. You can think of it this way:

T: Sync is the same as &T: Send


#3

Sorry, my mistake. I misread the complaint about Send in the compiler error as Sync. Thank you!

However, I found the following curious code snippet in libstd/thread/mod.rs, line 591:

fn _assert_sync_and_send() {
    fn _assert_both<T: Send + Sync>() {}
    _assert_both::<JoinHandle<()>>();
    _assert_both::<Thread>();
}

What’s the effect of calling _assert_both::<Thread>()?


#4

It’s a (hackish) way of statically (when compiling) asserting that a type implements a trait (or several traits).

As you can see, the function _assert_both() actually does nothing (and will be optimized away to a no-op). However, the type signature of the function requires the T type it’s instantiated with to implement both (hence the name) Send and Sync – if the type doesn’t, this will give a compile-time error. The code then proceeds with calling the function with T = JoinHandle and T = Thread, thus statically asserting that these types implement the traits.


#5

Thanks. Why does Thread need to implement Sync? You said that Sync [mostly?] isn’t used directly. :slight_smile:


#6

I mean that functions usually require Send, not Sync, as the primary use is for safely sending stuff between threads – see thread::spawn() and channel() for the two examples that come to mind. (But there are APIs that require Sync, for example Arc<T> won’t be Send unless T: Sync – the same logic as in &T: Send <=> T: Sync applies).

Sync is kind of on the other end: it’s understandable how a type comes to be Sync or !Sync. Basically, T: !Sync means that T employs interior mutability in a thread-unsafe way – whereas T: Send either doesn’t use it or uses it thread-safely.

Then to bridge the gap, a value is Send (that is, can be safely sent between threads), roughly speaking, if it doesn’t contain any references/pointers to !Sync data.

So to answer the question, Thread: Sync is just saying that it’s OK to fetch data about a thread in parallel (i.e. in several other threads) via a shared reference.

In fact, all the types would be both Send and Sync (because ownership/borrowing/lifetimes system guarantees safety) if it weren’t for interior mutability and unsafe code. I think I might want to write an article about all of that if I manage to bring my thoughts in order and happen to have time.