多執行緒

如果你使用多個執行緒 (Thread),你可以同時做很多事情。現代電腦有一個以上的核心 (Core),所以它們可以同時做多件事情,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 { // 設置十個執行緒
        std::thread::spawn(|| {
            println!("I am printing something");
        });
    }   // 現在執行緒啟動了.
}       // 有多少能在這裡的 main() 結束之前完成?

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 { // 讓電腦宣告 "let x = 9" 一百萬次
                            // 它要在它可以離開 main 函式前完成這件事
        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(); // 等待執行緒完成
    }
}

現在我們就來了解一下閉包的三種類型。這三種類型是

  • 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); // 現在 my_string 被拿來當參考使用
    });

    std::mem::drop(my_string);  // ⚠️ 我們嘗試在這丟棄它. 但執行緒仍然需要它.

    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);  // ⚠️ 我們無法丟棄, 因為 handle 擁有它. 因此這將會無法執行

    handle.join();
}

所以當我們把 std::mem::drop 刪掉,現在就可以用了。在 handle 拿走 my_string 後,我們的程式碼就安全了。

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

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

    handle.join().unwrap();
}

所以只要記住:如果你需要從外面取得某個執行緒裡面的值,你需要使用 move