安全高效的處理并發(fā)是 Rust 誕生的目的之一,主要解決的是服務(wù)器高負載承受能力。
并發(fā)(concurrent)的概念是只程序不同的部分獨立執(zhí)行,這與并行(parallel)的概念容易混淆,并行強調(diào)的是"同時執(zhí)行"。
并發(fā)往往會造成并行。
本章講述與并發(fā)相關(guān)的編程概念和細節(jié)。
線程(thread)是一個程序中獨立運行的一個部分。
線程不同于進程(process)的地方是線程是程序以內(nèi)的概念,程序往往是在一個進程中執(zhí)行的。
在有操作系統(tǒng)的環(huán)境中進程往往被交替地調(diào)度得以執(zhí)行,線程則在進程以內(nèi)由程序進行調(diào)度。
由于線程并發(fā)很有可能出現(xiàn)并行的情況,所以在并行中可能遇到的死鎖、延宕錯誤常出現(xiàn)于含有并發(fā)機制的程序。
為了解決這些問題,很多其它語言(如 Java、C#)采用特殊的運行時(runtime)軟件來協(xié)調(diào)資源,但這樣無疑極大地降低了程序的執(zhí)行效率。
C/C++ 語言在操作系統(tǒng)的最底層也支持多線程,且語言本身以及其編譯器不具備偵察和避免并行錯誤的能力,這對于開發(fā)者來說壓力很大,開發(fā)者需要花費大量的精力避免發(fā)生錯誤。
Rust 不依靠運行時環(huán)境,這一點像 C/C++ 一樣。
但 Rust 在語言本身就設(shè)計了包括所有權(quán)機制在內(nèi)的手段來盡可能地把最常見的錯誤消滅在編譯階段,這一點其他語言不具備。
但這不意味著我們編程的時候可以不小心,迄今為止由于并發(fā)造成的問題還沒有在公共范圍內(nèi)得到完全解決,仍有可能出現(xiàn)錯誤,并發(fā)編程時要盡量小心!
Rust 中通過 std::thread::spawn 函數(shù)創(chuàng)建新進程:
use std::thread; use std::time::Duration; fn spawn_function() { for i in 0..5 { println!("spawned thread print {}", i); thread::sleep(Duration::from_millis(1)); } } fn main() { thread::spawn(spawn_function); for i in 0..3 { println!("main thread print {}", i); thread::sleep(Duration::from_millis(1)); } }
運行結(jié)果:
main thread print 0 spawned thread print 0 main thread print 1 spawned thread print 1 main thread print 2 spawned thread print 2
這個結(jié)果在某些情況下順序有可能變化,但總體上是這樣打印出來的。
此程序有一個子線程,目的是打印 5 行文字,主線程打印三行文字,但很顯然隨著主線程的結(jié)束,spawn 線程也隨之結(jié)束了,并沒有完成所有打印。
std::thread::spawn 函數(shù)的參數(shù)是一個無參函數(shù),但上述寫法不是推薦的寫法,我們可以使用閉包(closures)來傳遞函數(shù)作為參數(shù):
use std::thread; use std::time::Duration; fn main() { thread::spawn(|| { for i in 0..5 { println!("spawned thread print {}", i); thread::sleep(Duration::from_millis(1)); } }); for i in 0..3 { println!("main thread print {}", i); thread::sleep(Duration::from_millis(1)); } }
閉包是可以保存進變量或作為參數(shù)傳遞給其他函數(shù)的匿名函數(shù)。閉包相當于 Rust 中的 Lambda 表達式,格式如下:
|參數(shù)1, 參數(shù)2, ...| -> 返回值類型 { // 函數(shù)體 }
例如:
fn main() { let inc = |num: i32| -> i32 { num + 1 }; println!("inc(5) = {}", inc(5)); }
運行結(jié)果:
inc(5) = 6
閉包可以省略類型聲明使用 Rust 自動類型判斷機制:
fn main() { let inc = |num| { num + 1 }; println!("inc(5) = {}", inc(5)); }
結(jié)果沒有變化。
use std::thread; use std::time::Duration; fn main() { let handle = thread::spawn(|| { for i in 0..5 { println!("spawned thread print {}", i); thread::sleep(Duration::from_millis(1)); } }); for i in 0..3 { println!("main thread print {}", i); thread::sleep(Duration::from_millis(1)); } handle.join().unwrap(); }
運行結(jié)果:
main thread print 0 spawned thread print 0 spawned thread print 1 main thread print 1 spawned thread print 2 main thread print 2 spawned thread print 3 spawned thread print 4
join 方法可以使子線程運行結(jié)束后再停止運行程序。
這是一個經(jīng)常遇到的情況:
use std::thread; fn main() { let s = "hello"; let handle = thread::spawn(|| { println!("{}", s); }); handle.join().unwrap(); }
在子線程中嘗試使用當前函數(shù)的資源,這一定是錯誤的!因為所有權(quán)機制禁止這種危險情況的產(chǎn)生,它將破壞所有權(quán)機制銷毀資源的一定性。我們可以使用閉包的 move 關(guān)鍵字來處理:
use std::thread; fn main() { let s = "hello"; let handle = thread::spawn(move || { println!("{}", s); }); handle.join().unwrap(); }
Rust 中一個實現(xiàn)消息傳遞并發(fā)的主要工具是通道(channel),通道有兩部分組成,一個發(fā)送者(transmitter)和一個接收者(receiver)。
std::sync::mpsc 包含了消息傳遞的方法:
use std::thread; use std::sync::mpsc; fn main() { let (tx, rx) = mpsc::channel(); thread::spawn(move || { let val = String::from("hi"); tx.send(val).unwrap(); }); let received = rx.recv().unwrap(); println!("Got: {}", received); }
運行結(jié)果:
Got: hi
子線程獲得了主線程的發(fā)送者 tx,并調(diào)用了它的 send 方法發(fā)送了一個字符串,然后主線程就通過對應(yīng)的接收者 rx 接收到了。