多线程

如果你使用多个线程,你可以同时做很多事情。现代计算机有一个以上的核心,所以它们可以同时做多件事情,Rust让你使用它们。Rust使用的线程被称为 "OS线程"。OS线程意味着操作系统在不同的核上创建线程。(其他一些语言使用 "green threads",功能较少)

你用std::thread::spawn创建线程,然后用一个闭包来告诉它该怎么做。线程很有趣,因为它们同时运行,你可以测试它,看看会发生什么。下面是一个简单的例子。

fn main() {
    std::thread::spawn(|| {
        println!("I am printing something");
    });
}

如果你运行这个,每次都会不一样。有时会打印,有时不会打印(这也取决于你的电脑速度)。这是因为有时main()在线程完成之前就完成了。而当main()完成后,程序就结束了。这在for循环中更容易看到。

fn main() {
    for _ in 0..10 { // set up ten threads
        std::thread::spawn(|| {
            println!("I am printing something");
        });
    }   // Now the threads start.
}       // How many can finish before main() ends here?

通常在main结束之前,大约会打印出四条线程,但总是不一样。如果你的电脑速度比较快,那么可能就不会打印了。另外,有时线程会崩溃。

thread 'thread 'I am printing something
thread '<unnamed><unnamed>thread '' panicked at '<unnamed>I am printing something
' panicked at 'thread '<unnamed>cannot access stdout during shutdown' panicked at '<unnamed>thread 'cannot access stdout during
shutdown

这是在程序关闭时,线程试图做一些正确的事情时出现的错误。

你可以给电脑做一些事情,这样它就不会马上关闭了。

fn main() {
    for _ in 0..10 {
        std::thread::spawn(|| {
            println!("I am printing something");
        });
    }
    for _ in 0..1_000_000 { // make the program declare "let x = 9" one million times
                            // It has to finish this before it can exit the main function
        let _x = 9;
    }
}

但这是一个让线程有时间完成的愚蠢方法。更好的方法是将线程绑定到一个变量上。如果你加上 let,你就能创建一个 JoinHandle。你可以在spawn的签名中看到这一点:

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

(f是闭包--我们将在后面学习如何将闭包放入我们的函数中)

所以现在我们每次都有JoinHandle

fn main() {
    for _ in 0..10 {
        let handle = std::thread::spawn(|| {
            println!("I am printing something");
        });

    }
}

handle现在是JoinHandle。我们怎么处理它呢?我们使用一个叫做 .join() 的方法。这个方法的意思是 "等待所有线程完成"(它等待线程加入它)。所以现在只要写handle.join(),它就会等待每个线程完成。

fn main() {
    for _ in 0..10 {
        let handle = std::thread::spawn(|| {
            println!("I am printing something");
        });

        handle.join(); // Wait for the threads to finish
    }
}

现在我们就来了解一下三种类型的闭包。这三种类型是

  • FnOnce: 取整个值
  • FnMut: 取一个可变引用
  • Fn: 取一个普通引用

如果可以的话,闭包会尽量使用Fn。但如果它需要改变值,它将使用 FnMut,而如果它需要取整个值,它将使用 FnOnceFnOnce是个好名字,因为它解释了它的作用:它取一次值,然后就不能再取了。

下面是一个例子。

fn main() {
    let my_string = String::from("I will go into the closure");
    let my_closure = || println!("{}", my_string);
    my_closure();
    my_closure();
}

String没有实现Copy,所以my_closure()Fn: 它拿到一个引用

如果我们改变my_string,它变成FnMut

fn main() {
    let mut my_string = String::from("I will go into the closure");
    let mut my_closure = || {
        my_string.push_str(" now");
        println!("{}", my_string);
    };
    my_closure();
    my_closure();
}

这个打印:

I will go into the closure now
I will go into the closure now now

而如果按值获取,则是FnOnce

fn main() {
    let my_vec: Vec<i32> = vec![8, 9, 10];
    let my_closure = || {
        my_vec
            .into_iter() // into_iter takes ownership
            .map(|x| x as u8) // turn it into u8
            .map(|x| x * 2) // multiply by 2
            .collect::<Vec<u8>>() // collect into a Vec
    };
    let new_vec = my_closure();
    println!("{:?}", new_vec);
}

我们是按值取的,所以我们不能多跑my_closure()次。这就是名字的由来。

那么现在回到线程。让我们试着从外部引入一个值:

fn main() {
    let mut my_string = String::from("Can I go inside the thread?");

    let handle = std::thread::spawn(|| {
        println!("{}", my_string); // ⚠️
    });

    handle.join();
}

编译器说这个不行。

error[E0373]: closure may outlive the current function, but it borrows `my_string`, which is owned by the current function
  --> src\main.rs:28:37
   |
28 |     let handle = std::thread::spawn(|| {
   |                                     ^^ may outlive borrowed value `my_string`
29 |         println!("{}", my_string);
   |                        --------- `my_string` is borrowed here
   |
note: function requires argument type to outlive `'static`
  --> src\main.rs:28:18
   |
28 |       let handle = std::thread::spawn(|| {
   |  __________________^
29 | |         println!("{}", my_string);
30 | |     });
   | |______^
help: to force the closure to take ownership of `my_string` (and any other referenced variables), use the `move` keyword
   |
28 |     let handle = std::thread::spawn(move || {
   |                                     ^^^^^^^

这条信息很长,但很有用:它说到use the `move` keyword。问题是我们可以在线程使用my_string时对它做任何事情,但线程并不拥有它。这将是不安全的。

让我们试试其他行不通的东西。

fn main() {
    let mut my_string = String::from("Can I go inside the thread?");

    let handle = std::thread::spawn(|| {
        println!("{}", my_string); // now my_string is being used as a reference
    });

    std::mem::drop(my_string);  // ⚠️ We try to drop it here. But the thread still needs it.

    handle.join();
}

所以你要用move来取值,现在安全了:

fn main() {
    let mut my_string = String::from("Can I go inside the thread?");

    let handle = std::thread::spawn(move|| {
        println!("{}", my_string);
    });

    std::mem::drop(my_string);  // ⚠️ we can't drop, because handle has it. So this won't work

    handle.join();
}

所以我们把std::mem::drop删掉,现在就可以了。handlemy_string,我们的代码就安全了。

fn main() {
    let mut my_string = String::from("Can I go inside the thread?");

    let handle = std::thread::spawn(move|| {
        println!("{}", my_string);
    });

    handle.join();
}

所以只要记住:如果你在线程中需要一个来自线程外的值,你需要使用move