Search code examples
rustclosuresownership

How can a closure outlive the main function if the entire process ends when main function ends?


I have below code:

use std::thread;

fn main() {
    let x: &'static mut [i32; 3] = Box::leak(Box::new([1, 2, 3]));
    thread::spawn(|| dbg!(&x));
}

When I compile it, I get below error:

error[E0373]: closure may outlive the current function, but it borrows `x`, which is owned by the current function
--> src\main.rs:10:19
|
10 |     thread::spawn(|| dbg!(&x));
|                   ^^       - `x` is borrowed here
|                   |
|                   may outlive borrowed value `x`
|
note: function requires argument type to outlive `'static`
--> src\main.rs:10:5
|
10 |     thread::spawn(|| dbg!(&x));
|     ^^^^^^^^^^^^^^^^^^^^^^^^^^
help: to force the closure to take ownership of `x` (and any other referenced variables), use the `move` keyword
|
10 |     thread::spawn(move || dbg!(&x));
|                   ++++

Here it tells me "closure may outlive the current function". I suppose the "current function" is the main function. But is it possible that the closure outlives the main function? When the main function ends, the entire process ends as well. This means that all threads that were spawned from this process are also terminated, regardless of whether they have finished executing or not. So how can the closure outlive the main function?


Solution

  • Threads are killed after the main function ended.

    This sounds like nit-picking, but this distinction is important, because the ending of the main function includes the destruction of local variables. This means first the variables are destroyed, including calling their drop() implementations, and then the threads get killed.

    This demonstrates that fact:

    use std::{
        thread::{self, sleep},
        time::Duration,
    };
    
    struct MyStruct {}
    
    impl Drop for MyStruct {
        fn drop(&mut self) {
            println!("Drop");
            sleep(Duration::from_millis(100));
        }
    }
    
    fn main() {
        let _x = MyStruct {};
    
        thread::spawn(|| loop {
            sleep(Duration::from_millis(45));
            println!("Thread");
        });
    
        sleep(Duration::from_millis(100));
    }
    
    Thread
    Thread
    Drop
    Thread
    Thread
    

    Note that if the thread had a & (non-mut) reference to _x, inside of the drop() function, a & and a &mut to _x would exist simultaneously. Further, whatever content the variable contains will most certainly be destroyed in the drop() function, so having an external reference to it is most definitely undefined behaviour.

    For reasons like that, the main() function simply gets treated as a normal function, and all the normal borrowing rules apply.


    Those were some general words about why main() gets treated as a normal function, but now let's talk about your code.

    It seems you tried to overcome this issue by Box::leaking the variable. This is absolutely a possible way of prolonging the lifetime of a variable past the end of main(), but you made a mistake. The array itself is &'static, but the variable x is not. And when you do &x in your thread, you are not referencing the array, but the variable x, and x lives in main().

    You need to move the reference itself into the thread. Because it isn't obvious how this differs from moving the Box itself into the thread, I'll create a second thread for demonstration purposes:

    use std::{
        thread::{self, sleep},
        time::Duration,
    };
    
    fn main() {
        let x: &'static [i32; 3] = Box::leak(Box::new([1, 2, 3]));
        let x2 = x;
    
        thread::spawn(move || dbg!(x));
        thread::spawn(move || dbg!(x2));
    
        sleep(Duration::from_millis(10));
    }
    
    [src\main.rs:10] x = [
        1,
        2,
        3,
    ]
    [src\main.rs:11] x2 = [
        1,
        2,
        3,
    ]
    

    I also made the reference &, not &mut, to be used by two threads. Also note that & references are Copy, making let x2 = x; possible. This does not clone the original array, they both point at the very same data.

    When paired with a Mutex, this becomes obvious:

    use std::{
        sync::Mutex,
        thread::{self, sleep},
        time::Duration,
    };
    
    fn main() {
        let x: &'static Mutex<[i32; 3]> = Box::leak(Box::new(Mutex::new([1, 2, 3])));
        let x2 = x;
    
        thread::spawn(move || {
            sleep(Duration::from_millis(50));
            let locked_x = x.lock().unwrap();
            dbg!(*locked_x);
        });
        thread::spawn(move || {
            x2.lock().unwrap()[1] = 42;
        });
    
        sleep(Duration::from_millis(100));
    }
    
    [src\main.rs:14] *locked_x = [
        1,
        42,
        3,
    ]