DEV Community

BC
BC

Posted on

Day17:Thread & Closure - 100DayOfRust

First try, define a mutable vector then pass a closure to thread, inside closure, push another value to vector.

use std::{thread, time};

fn main() {
    let mut numbers = vec![1, 2, 3]; 
    let t1 = thread::spawn(|| {
        thread::sleep(time::Duration::from_secs(1));
        numbers.push(4);
        println!("numbers: {:?}", numbers);
    }); 
    t1.join().unwrap();
    println!("numbers: {:?}", numbers);
}
Enter fullscreen mode Exit fullscreen mode

If we run the code, we got errors:

error[E0373]: closure may outlive the current function, but it borrows `numbers`, which is owned by the current function
 --> thread1.rs:5:28
  |
5 |     let t1 = thread::spawn(|| {
  |                            ^^ may outlive borrowed value `numbers`
6 |         thread::sleep(time::Duration::from_secs(1));
7 |         numbers.push(4);
  |         ------- `numbers` is borrowed here
...
5 |     let t1 = thread::spawn(move || {
  |                            ^^^^^^^

error[E0502]: cannot borrow `numbers` as immutable because it is also borrowed as mutable
...
10 |       t1.join().unwrap();
11 |       println!("numbers: {:?}", numbers);
   |                                 ^^^^^^^ immutable borrow occurs here
Enter fullscreen mode Exit fullscreen mode

We got 2 errors here: the first one "closure may outlive the current function", this is because when we operate the numbers inside closure in a thread, the main thread might drop numbers, then the thread closure will refer to an invalid memory address. To solve this problem, we need to move the ownership of numbers to closure, such that Rust will know only the thread closure will use the numbers. Let's change the code:

...
let t1 = thread::spawn(move || {
...
Enter fullscreen mode Exit fullscreen mode

Let's compile the code again, but still we have an error:

error[E0382]: borrow of moved value: `numbers`
  --> thread1.rs:11:31
   |
4  |     let mut numbers = vec![1, 2, 3];
   |         ----------- move occurs because `numbers` has type `std::vec::Vec<i32>`, which does not implement the `Copy` trait
5  |     let t1 = thread::spawn(move || {
   |                            ------- value moved into closure here
6  |         thread::sleep(time::Duration::from_secs(1));
7  |         numbers.push(4);
   |         ------- variable moved due to use in closure
...
11 |     println!("numbers: {:?}", numbers);
   |                               ^^^^^^^ value borrowed here after move
Enter fullscreen mode Exit fullscreen mode

Because we move the ownership of numbers to thread closure, the outside scope doesn't have the ownership of numbers, it cannot use the numbers anymore. Let's remove that line:

use std::{thread, time};

fn main() {
    let mut numbers = vec![1, 2, 3];
    let t1 = thread::spawn(move || {
        thread::sleep(time::Duration::from_secs(1));
        numbers.push(4);
        println!("numbers: {:?}", numbers);
    });
    t1.join().unwrap();
}
Enter fullscreen mode Exit fullscreen mode

This time it can run:

numbers: [1, 2, 3, 4]
Enter fullscreen mode Exit fullscreen mode

Let's try another example:

use std::{thread, time};

fn main() {
    let duration = time::Duration::from_secs(1);
    let t1 = thread::spawn(|| {
        thread::sleep(duration);
        println!("hello from spawned thread");
    });
    t1.join().unwrap();
    thread::sleep(duration);
    println!("hello from main thread");
}
Enter fullscreen mode Exit fullscreen mode

The code has a similar logic like the first one, but inside the thread closure, instead of using vector, we used the Duration instance.

Compile it, we got an error:

error[E0373]: closure may outlive the current function, but it borrows `duration`, which is owned by the current function
 --> thread2.rs:5:28
  |
5 |     let t1 = thread::spawn(|| {
  |                            ^^ may outlive borrowed value `duration`
6 |         thread::sleep(duration);
  |                       -------- `duration` is borrowed here
  |
Enter fullscreen mode Exit fullscreen mode

That is as expected, the closure using the duration in the outer scope, Rust suggested us to use the move keyword. Let's add it:

use std::{thread, time};

fn main() {
    let duration = time::Duration::from_secs(1);
    let t1 = thread::spawn(move || {
        thread::sleep(duration);
        println!("hello from spawned thread");
    });
    t1.join().unwrap();
    thread::sleep(duration);
    println!("hello from main thread");
}
Enter fullscreen mode Exit fullscreen mode

Let's re-run the code:

hello from spawned thread
hello from main thread
Enter fullscreen mode Exit fullscreen mode

It works! But wait, we are using duration not only in the thread closure, but also in the outer scope, why don't we have the error like in the first example? Remember in the first example, we are not allowed to use the numbers in the outer scope after we move the ownership to the thread closure. But in this example, we don't have such error and the code runs.

The reason is, for the Duration type, move won't really move the ownership, instead it will copy that variable to closure, such that both the thread closure and outer scope can use that variable.

So what move will do is:

  • First check if this variable implemented the Copy trait, if the variable does, then move will do a variable copy and move the copy to the closure. In this case, the closure and outer scope can both use this variable.
  • If this variable doesn't implement the Copy trait, move will move the ownership of that variable to thread closure. In this case, the outer scope cannot use the variable anymore.

To confirm this, if we dig into the source code of Duration type[1], we can see:

#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
pub struct Duration {
    secs: u64,
    nanos: u32, // Always 0 <= nanos < NANOS_PER_SEC
}
Enter fullscreen mode Exit fullscreen mode

The magic happens in the code #[derive(Clone, Copy)]. We mentioned the Copy trait above, but what's this Clone? Well, Clone is also required, as it's a supertrait of Copy.[2]

Let's see another example, this time we are going to use our own struct.

use std::{thread, time};

#[derive(Debug)]
struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let duration = time::Duration::from_secs(1);
    let p = Point {x: 1, y:2};
    let t1 = thread::spawn(move || {
        thread::sleep(duration);
        println!("[Spawned thead] Point {:?}", p);
    });
    t1.join().unwrap();
    thread::sleep(duration);
    println!("[Main thead] Point {:?}", p);
}
Enter fullscreen mode Exit fullscreen mode

We defined a struct called "Point" and created an instance p, then in thread closure, we used move and print out p, in the main thread, we also printed p.

Compile it we got the error:

error[E0382]: borrow of moved value: `p`
  --> thread3.rs:18:41
   |
11 |     let p = Point {x: 1, y:2};
   |         - move occurs because `p` has type `Point`, which does not implement the `Copy` trait
12 |     let t1 = thread::spawn(move || {
   |                            ------- value moved into closure here
13 |         thread::sleep(duration);
14 |         println!("[Spawned thead] Point {:?}", p);
   |                                                - variable moved due to use in closure
...
18 |     println!("[Main thead] Point {:?}", p);
   |                                         ^ value borrowed here after move

Enter fullscreen mode Exit fullscreen mode

Review the move rule we wrote above: since Point doesn't implement the Copy trait, so move will move the ownership of p, then the outer scope cannot use the p anymore.

There are 2 ways to implement the Copy trait to our struct [3], the easiest way if to use the derive, just like the one in the Duration source code. So change #[derive(Debug)] to #[derive(Debug, Clone, Copy)]. Here is the complete code:

use std::{thread, time};

#[derive(Debug, Clone, Copy)]
struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let duration = time::Duration::from_secs(1);
    let p = Point {x: 1, y:2};
    let t1 = thread::spawn(move || {
        thread::sleep(duration);
        println!("[Spawned thead] Point {:?}", p);
    });
    t1.join().unwrap();
    thread::sleep(duration);
    println!("[Main thead] Point {:?}", p);
}
Enter fullscreen mode Exit fullscreen mode

Compile and run the code, it works!

[Spawned thead] Point Point { x: 1, y: 2 }
[Main thead] Point Point { x: 1, y: 2 }
Enter fullscreen mode Exit fullscreen mode

Reference

  1. Duration Type
  2. Copy & Clone Trait
  3. How to implement Copy

Top comments (0)