A little confused with multithreading

use std::sync::mpsc;
use std::thread;

fn main()
{
    let (tx, rx) = mpsc::channel();

    thread::spawn(move || // What does the two pipe symbols '||' mean and what does 'move()' even do?
    {
        let val = String::from("Hello");
        tx.send(val).unwrap(); // What does 'unwrap()' even mean?
    });

    let received = rx.recv().unwrap();

    println!("{}", received);
}

So I am confused with a few things with this code. So inside the thread::spawn(move ||.. function, what does the move keyword even do and what does || even do inside the function?

And additionally what does unwrap() exactly do?

https://doc.rust-lang.org/book/ch13-01-closures.html

https://doc.rust-lang.org/stable/std/result/enum.Result.html#method.unwrap

If something is not clear in provided links, please ask more specifically, it's hard to see what you don't understand for now.

It was from some video tutorial and the guy fails to explain clearly but I will check out the links, thanks.

For unwrap() in the link that you posted, it basically is used to return ok or not. So why is it when I comment it out the unwrap() in tx.send(val).unwrap(); I get an error?

|| is used to pass in arguments for closures which I understand, however why are we using || if we aren't passing in any arguments?

What is this error? Does it have any hints from the compiler? It's entirely possible that they will literally answer your question here.

|| is used to define the closure, just like () are used to define the function. It's part of a syntax.

use std::sync::mpsc;
use std::thread;

fn main()
{
    let (tx, rx) = mpsc::channel();

    thread::spawn(move || // What does the two pipe symbols '||' mean and what does 'move()' even do?
    {
        let val = String::from("Hello");
        tx.send(val).unwrap(); // What does 'unwrap()' even mean?
    });

    let received = rx.recv();//.unwrap();

    println!("{}", received);
}

So this is my new code.

And this is the error:

error[E0277]: `std::result::Result<std::string::String, std::sync::mpsc::RecvError>` doesn't implement `std::fmt::Display`
  --> src\main.rs:16:20
   |
16 |     println!("{}", received);
   |                    ^^^^^^^^ `std::result::Result<std::string::String, std::sync::mpsc::RecvError>` cannot be formatted with the default formatter
   |
   = help: the trait `std::fmt::Display` is not implemented for `std::result::Result<std::string::String, std::sync::mpsc::RecvError>`
   = note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead
   = note: required by `std::fmt::Display::fmt`
   = note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info)

error: aborting due to previous error

I don't quite get what the error message means?

To my simple mind it goes like this:

spawn() takes a function as a parameter. That is the function you want to run in it's own thread.

Normally we define functions with "fn someName (...) {...}" but in this case we don't need a name for the function and that "fn" just gets in the way.

So we just have an anonymous function body {...} to put in there as the parameter to spawn().

But we still need a way to indicate it is a function and give it parameters. That is done with "| |". Which sometimes might have parameters inside it.

That function, as well as possibly having parameters passed in "| |" may also need access to other variables in the scope of that spawn call. That is what "move" does. In your example that thread function needs access to the tx channel, so it needs to be moved in.

The unwrap() is there because tx.send() returns and error indication that Rust expects you to handle. If you don't want to handle it yourself then .unwrap() pulls the error code out of the return value and discards it. Unless there is actually and error, in which case there is a panic and your program dies.

1 Like

It means exactly what is written: Rust doesn't know how to print the Result<String, RecvError> returned by recv().

Thing is rx.recv(); is not returning a simple value like a u32 or whatever. It returns and Result object. I think it is actually an enum. It can contain an error value if there is a failure of a String if success.

The compiler error message says that println! does not know how to print that complicated Result object.

That is why we have "unwrap()". It pulls the simple String value out of the result object (Unless there is actually an error, in which case it panics). println!() knows how to print strings.

2 Likes

Makes perfect sense, thanks mate.

So does the keyword move take ownership or does it borrow ownership of all the variables declared above the thread::move() function?

Ah I see, so basically Rust is forcing me to have some error handler therefore I need that unwrap() function, right?

First, not of all declared above, but of all used in closure. Second, without move closure will borrow by default (when it can) and move only if it needs to (i.e. if it have to move the value further), with move - only move.

Right.

1 Like

I see, thanks man.

Oh, right man that makes more sense.

When I remove the move keyword why do I get an error.

error[E0277]: `std::sync::mpsc::Sender<std::string::String>` cannot be shared between threads safely
   --> src\main.rs:8:5
    |
8   |     thread::spawn(|| // What does the two pipe symbols '||' mean and what does 'move()' even do?
    |     ^^^^^^^^^^^^^ `std::sync::mpsc::Sender<std::string::String>` cannot be shared between threads safely
    | 
   ::: C:\Users\joe\.rustup\toolchains\stable-x86_64-pc-windows-msvc\lib/rustlib/src/rust\src\libstd\thread\mod.rs:616:8
    |
616 |     F: Send + 'static,
    |        ---- required by this bound in `std::thread::spawn`
    |
    = help: the trait `std::marker::Sync` is not implemented for `std::sync::mpsc::Sender<std::string::String>`
    = note: required because of the requirements on the impl of `std::marker::Send` for `&std::sync::mpsc::Sender<std::string::String>`
    = note: required because it appears within the type `[closure@src\main.rs:8:19: 12:6 tx:&std::sync::mpsc::Sender<std::string::String>]`

error: aborting due to previous error

For more information about this error, try `rustc --explain E0277`.

I don't get why it can't be shared between threads safely?

Because authors of standard library decided so (note the !Sync impl). Why is this the case - well, maybe Sender holds some internal data and does not synchronize access to it, and so using it from multiple threads simultaneously would lead to data race.

1 Like

Even if you use a type that can be shared between threads safely, the + 'static bound will prevent it from working without move, as the closure only has a reference to the channel, and the compiler cannot verify that this reference remains valid for the duration of the new thread, as it might outlive the thread it is spawned in.

1 Like

There's nothing wrong with this conclusion per se, but you will find out that Rust also helps you reduce error-handling boilerplate by providing many useful functionalities — the ? operator, for example. Here's a demonstration using a Box < dyn Error > to capture all errors:

use std::error::Error;
use std::sync::mpsc;
use std::thread;

// convenience type alias
type Result<T, E = Box<dyn 'static + Error + Send + Sync>> = std::result::Result<T, E>;

fn main() -> Result<()> {
    let (tx, rx) = mpsc::channel();

    thread::spawn(move || -> Result<()> {
        let string = "Hello".to_string();
        tx.send(string)?;
        Ok(())
    });

    let string = rx.recv()?;
    println!("{}", string);

    Ok(())
}

(playground)

In addition, Result contains a lot of handy methods that can be used.

1 Like

"it" in this case is an mpsc::Sender.

It kind of makes sense to me that a channel, like an mpsc, is intended to link to thread to another. One thread has the sender end of the channel the other thread has the receiver. There would be no point in having the sender or receiver channel ends owned by the same thread.

The whole point of these channels is to share data safely. In your example a string. Having the sender in one thread and the receiver in the other, exclusively is how channels implement that safe sharing.

Of course mpsc is a multi-producer system so you can have multiple sender ends. One for each producer thread. Perhaps one. I seem to remember that involves some cloning of the sender. Perhaps a single thread could own two senders. I don't know.

1 Like

Oh ok I see, but then why can't I borrow ownership instead of taking ownership?

Good question.

The way I see it when a function starts a thread that thread may well run for far longer than the function takes to execute. Potentially forever.

As such it makes no sense to loan out anything to the thread. The compiler has no way to know when the thread will terminate and the loan be paid back.

Imagine this:

Your function could spawn two threads and then terminate. When spawning it could give (move) a channel sender to one thread and a receiver to the other. Those threads could run forever.

What would it mean to only borrow those channels ends? There is nobody to give them back to if/when the threads terminate.

2 Likes

Note also that your main function isn't special, so the argument like "when I exit from main, all spawned threads will terminate" won't work.

1 Like

On a more concrete level, the owner of an object is responsible for allocating and cleaning up that object’s memory; in the case of a local variable, it’s stored on the stack. Any references to that variable, then, must be destroyed by the time the function returns. Otherwise, they’re referring to something that no longer exists.

If you want to make an object that lives beyond the end of the function that creates it, you have to transfer ownership of that object somewhere else. There are many ways to do this, such as:

  • You can transfer ownership to the calling function with return
  • You can store the object in a collection you have an &mut reference to
  • You can (sort of) give ownership to the operating system with Box::leak
  • You can send it to another thread with an mpsc::Sender

Regardless of the method you use, there can’t be any outstanding references to the object that’s being transferred because its location in memory might move in the process.

1 Like