Why does the field in a struct need to be in a mutex?


#1

The following code refuses to compile unless I put the sender field into a Mutex.

I am not exactly sure why? That field is not being used within the spawned thread. Also the receiver variable is passed through quite happily as a parameter without needing to be Mutexed… If I put the receiver into the struct rather as a parameter, then that will need to be within a Mutex too.

So there is something about it being a part of the struct that means it has to be locked. Any ideas?

Thanks :slight_smile:

use std::io;
use std::thread::spawn;
use std::sync::{Arc, Mutex};
use std::sync::mpsc::{channel, Sender, Receiver};
   
#[derive(Debug)]
struct Amazing {
    stuff: Arc<Mutex<Vec<u32>>>,
    sender: Mutex<Sender<u32>>
}

fn run(a: &Arc<Amazing>, receiver: Receiver<u32>) -> io::Result<()> {
    let a = a.clone();
    spawn(move || {
        let stuff = a.stuff.clone();
        
        for i in receiver {
            let mut stuff = stuff.lock().unwrap();
            stuff.push(i)
        }
    });
    
    Ok(())
}

fn main() {
    let (sender, receiver) = channel();
    let amazing = Arc::new(Amazing { stuff: Arc::new(Mutex::new(vec![])),
                                     sender: Mutex::new(sender) });
    
    run(&amazing, receiver).unwrap();

    let amazing = amazing.clone();
    let sender = amazing.sender.lock().unwrap();
    sender.send(3).unwrap();
    sender.send(9).unwrap();
    sender.send(2).unwrap();

    std::thread::sleep(std::time::Duration::from_secs(1));
    println!("{:?}", amazing);
}

#2

Let’s start with a longer explanation first.

This is due to requirements on thread::spawn(), mpsc::Sender, and Arc. Let’s break them down:

  1. spawn() wants the closure F given to it to be F: Send + 'static (the Send part is important)
  2. You’re capturing an Arc in the closure given to spawn(), so it needs to be Send due to #1
  3. Arc<T> is Send when T: Send + Sync. T here is your Amazing struct.
  4. mpsc::Sender is explicitly !Sync, which makes your Amazing not Sync - this fails the requirements.

Since Sender is Send, a Mutex<T> is Sync if T: Send - this means putting the Sender inside a mutex makes your Amazing Sync, and satisfies the requirements.

The short of it is that your spawn() closure has an Arc<Amazing> in it because you move'd the a clone into it. Even though you then proceed to just use stuff and not sender, the compiler doesn’t look at it in those terms. As far as it’s concerned, your closure contains a Arc<Amazing> and that needs to satisfy the obligations/requirements above.

Anyway, I’d restructure your code like this:

use std::io;
use std::thread::spawn;
use std::sync::{Arc, Mutex};
use std::sync::mpsc::{channel, Receiver, Sender};

#[derive(Debug)]
struct Amazing {
    stuff: Arc<Mutex<Vec<u32>>>,
    sender: Sender<u32>,
}

fn run(a: &Amazing, receiver: Receiver<u32>) -> io::Result<()> {
    let stuff = a.stuff.clone();
    spawn(move || {
        for i in receiver {
            let mut stuff = stuff.lock().unwrap();
            stuff.push(i)
        }
    });

    Ok(())
}

fn main() {
    let (sender, receiver) = channel();
    let amazing = Amazing {
        stuff: Arc::new(Mutex::new(vec![])),
        sender,
    };

    run(&amazing, receiver).unwrap();

    let sender = &amazing.sender;
    sender.send(3).unwrap();
    sender.send(9).unwrap();
    sender.send(2).unwrap();

    std::thread::sleep(std::time::Duration::from_secs(1));
    println!("{:?}", amazing);
}

#3

Ah I see. So it is putting the Sender into an Arc rather than a struct that causes the problem. Putting a !Sync into an Arc trashes its Send?

I think that will take a little while to sink in, but I think I understand the principles.

I like your solution. It makes it much simpler pulling only what you need into the closure.

Thanks.


#4

Right; putting the Sender into your struct makes your struct !Sync. That’s fine if you don’t need your struct to be Sync. But …

Yup.

Normally, most types are Send and Sync and it’s completely fine to share them across threads via an Arc. mpsc::Sender, however, is not Sync. Instead, it’s Clone + Send. To have multiple producers across threads sending to a single receiver, you would clone() the sender and then move (ie Send) it to a thread; each thread would have its own Sender value (all of them operating on the same underlying channel, of course).

Besides being simpler and easier to grok mentally, you’ll also find that splitting things fairly granularly will help you with Rust code in general, even outside of multithreading. For instance, taking borrows of individual fields of a struct, rather than the entire struct, will please the borrow checker more frequently, and you in turn :slight_smile: .


#5

Interesting, thanks. Yes that does make sense… You need to be more explicit about what is happening to all the individual components of your data. I can see that I am going to have to rethink the way I structure my programs.