編寫測試可以讓我們的代碼在後續迭代過程中不出現功能性缺陷問題;理解迭代器、閉包的函數式編程特性;`Box ...
編寫測試可以讓我們的代碼在後續迭代過程中不出現功能性缺陷問題;理解迭代器、閉包的函數式編程特性;Box<T>
智能指針在堆上存儲數據,Rc<T>
智能指針開啟多所有權模式等;理解併發,如何安全的使用線程,共用數據。
自動化測試
編寫測試以方便我們在後續的迭代過程中,不會改壞代碼。保證了程式的健壯性。
測試函數通常進行如下操作:
- 設置需要的數據或狀態
- 運行需要測試的代碼
- 斷言其結果是我們期望的
在 rust 中,通過test
屬性、斷言巨集和一些屬性設置來測試代碼。
$> cargo new ifun-grep --lib
創建項目時,通過--lib
表明創建一個庫,會預設生成一個測試示例,在src/lib.rs
中
pub fn add(left: usize, right: usize) -> usize {
left + right
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_works() {
let result = add(2, 2);
assert_eq!(result, 4);
}
}
進入到項目中,執行cargo test
就會看到執行完測試的詳細信息。包括了測試數量、通過測試數、失敗測試數等等維度
首先使用mod tests
定義了一個 tests 模塊,內部函數需要使用外部方法,在最頂部調用了use super::*;
。這在包的一節里已有說明。
#[cfg(test)]
標註測試模塊。它可以告訴 rust 在編譯時不需要包含該測試代碼。
#[test]
表明是測試函數,通過 assert_eq!()
斷言結果值是否相同。
可以手動改動一下斷言值assert_eq!(result, 5)
,再次執行可以看到測試不通過,並給出了結果的不同之處。
由 rust 標準庫提供的斷言測試巨集,幫助我們處理結果值。結果與預期相同時,則測試會通過;不一樣時,則會調用panic!
巨集,導致測試失敗。
assert!()
一個必傳參數,true
是測試通過;false
測試失敗。assert_eq!()
兩個必傳參數,比對它們是否相同。assert_ne!
兩個必傳參數,比對它們是否不相同。
assert_eq!
和assert_ne
斷言失敗時,會列印出兩個值,便於觀察為什麼失敗。因為會列印輸出,所以兩個值必須實現PartialEq
和Debug trait
可以被比較和輸出調試。
如果是我們自定義的結構體或枚舉類型,則可以直接增加#[derive(PartialEq, Debug)]
註解。如果是複雜的類型,則需要派生巨集trait
,這在後面的文章會講。
#[derive(PartialEq,Debug)]
struct User {
name: String,
}
巨集除了它們必須的參數之外,也可以傳遞更多的參數,這些參數會被傳遞給format!()
列印輸出。這樣我們可以增加一些輸出,方便解決斷言失敗的問題
assert_eq!(result, 5, "hello rust!");
測試程式處理錯誤
除了測試程式正常執行邏輯的結果,也需要測試程式發生錯誤時,是否按照我們的錯誤處理邏輯 處理了錯誤。
假設我們的被測試函數接受的參數不能大於100
,大於時panic
錯誤 信息
pub fn add(left: usize, right: usize) -> usize {
if right > 100 {
panic!("the value exceeds 100!");
}
left + right
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_works() {
let result = add(2, 102);
assert_eq!(result, 104);
}
}
執行測試cargo test
,就算斷言結果時邏輯正確的,但是我們的函數限制了參數最大值,測試不通過。
增加測試用例來測試這種場景,通過增加#[should_panic]
來處理程式確實有這種限制,並panic!
。
#[test]
#[should_panic]
fn value_exceed_100() {
add(5, 120);
}
執行cargo test
,可以看到測試示例通過了。如果我們符合參數要求,測試示例就會是失敗
但如果我們代碼中有多個錯誤panic!()
,就會有同樣的多個測試示例不通過,列印輸出並沒有給我們足夠的信息去找到問題所在。
通過should_panic
可選擇參數expected
提供一個錯誤描述信息,
pub fn add(left: usize, right: usize) -> usize {
if right > 100 {
panic!("the value exceeds 100!,got {}", right)
} else if right < 50 {
panic!("the value does not less than 50!,got {}", right)
}
left + right
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[should_panic(expected = "exceeds 100!")]
fn value_exceed_100() {
add(5, 99);
}
#[test]
#[should_panic(expected = "less than 50!")]
fn value_not_less_50() {
add(59, 59);
}
}
也可以通過Result<T,E>
編寫測試,在程式失敗時,返回Err
而不是panic
;
#[test]
fn add_equal() -> Result<(), String> {
if add(5, 105) == 111 {
Ok(())
} else {
Err(String::from("Error in add"))
}
}
此時不能使用#[should_panic()]
註解。也不能使用表達式?
控制測試運行
cargo test
在測試模式下編譯代碼併發運行生成的測試二進位文件。
- 可以通過設置測試線程,單次只執行一個測試示例
$> cargo test -- --test-threads=1
測試線程為 1,程式不會使用任何並行機制。
- 預設的測試在測試示例通過時,不會列印輸出。通過設置在測試成功時也輸出程式中的列印
$> cargo test -- --show-output
- 預設的
cargo test
會運行所有測試,通過指定名稱來運行部分測試
$> cargot test add_equal
過濾運行多個測試,可以通過指定測試名稱的一部分,只要匹配這個名稱的測試都會被運行。
$> cargot test value
通過#[ignore]
標記忽略該測試。
#[test]
#[ignore]
fn add_equal() -> Result<(), String> {
if add(5, 105) == 110 {
Ok(())
} else {
Err(String::from("Error in add"))
}
}
測試被忽略,但是可以通過cargot test -- --ignored
來運行被忽略的測試。
如果想運行所有的測試,可以通過cargot test -- --include-ignored
集成測試
單元測試可以在指定的模塊中書寫測試實例,每次測試一個模塊,也可以測試私有介面。
集成測試對庫來說是外部的,只能測試公有介面,可測試多個模塊。通過創建tests
目錄編寫獨立的測試文件。
tests/lib.rs
use ifun_grep;
#[test]
#[should_panic(expected = "exceeds")]
fn value_exceed_100() {
ifun_grep::add(5, 99);
}
隨著集成測試模塊的增多,我們需要更好的組織它們,可以根據測試的功能將測試分組。將一些測試公共模塊抽離出來,作為其他測試功能組的測試函數調用
比如tests/common.rs
pub fn init(){
// something init
}
再執行cargo test
,會看到運行了tests/common.rs
運行了 0 個測試。這顯然是我們不需要的,可以改寫文件目錄tests/common/mod.rs
,這會告訴 rust 不要將common
看作一個集成測試文件。
迭代器與閉包
rust 類似函數式編程語言的特性。可以將函數作為參數值或返回值、將函數賦值給變數等。
閉包
可以儲存在變數里的類似函數的結構。保存在一個變數中或作為參數傳遞給其他函數的匿名函數。
閉包允許捕獲被定義時所在作用域中的值。
#[derive(Debug)]
enum Name {
Admin,
Test,
}
#[derive(Debug)]
struct User {}
impl User {
fn get_name(&self, name: Option<Name>) -> Name {
name.unwrap_or_else(|| self.random_name())
}
fn random_name(&self) -> Name {
Name::Admin
}
}
fn main(){
let user = User {};
println!("{:?}", user.get_name(Some(Name::Test)));
println!("{:?}", user.get_name(None));
}
unwrap_or_else
方法接受一個閉包函數,當一個Some
值存在時直接返回,如果不存在則執行其傳入的閉包函數計算一個值返回。
閉包不需要在參數或返回值上註明類型。閉包通常只關聯小範圍的上下文而非任意情景,所以編譯器可以推導出參數和返回值類型。
也可以顯示定義閉包的參數和返回值的類型:
fn main(){
let get_age = |age: i8| -> i8 { age };
// let get_age = |age| age;
println!("{}", get_age(32));
}
相對於增加參數或返回值類型使得書寫更加的繁瑣。而對於未標註類型的閉包,在第一次調用後就確定其參數和返回值類型,再傳其他類型時就會報錯。
fn main(){
let get_age = |age| age;
println!("{}", get_age(String::from("admin")));
// 調用出錯,已經確定了參數和返回值類型為String
println!("{}", get_age(32));
}
捕獲引用或移動所有權
在傳遞給閉包參數時,需要考慮參數的傳遞方式:不可變借用、可變借用和獲取所有權。這是根據傳遞的值決定的。
對於不可變借用,變數可以在任何情形下被訪問。
let str = String::from("hboot");
let print_str = || println!("{:?}", str);
println!("{str}");
print_str();
println!("{str}");
而對於可變借用,則只能在借用結束後調用.聲明的閉包函數也需要mut
聲明
let mut str = String::from("hboot");
let mut print_str = || str.push_str("-rust");
// println!("{str}");
print_str();
println!("{str}");
通過move
關鍵字將變數的所有權轉移閉包所在的環境中。
use std::thread;
fn main(){
let mut str = String::from("hboot");
println!("{str}");
thread::spawn(move || {
str.push_str("-rust");
println!("{str}")
})
.join()
.unwrap();
}
此時,將變數str
值的所有權轉移到了新線程中,主線程則不能再使用。
將被捕獲的值移出閉包和 Fn trait
在閉包環境中,捕獲和處理值的方式會影響閉包 trait 的實現。trait 是函數或結構體指定它們可以使用什麼類型的閉包。
從閉包如何任何處理值、閉包自動、漸進實現一個、多個 Fn
trait
FnOnce
適用於調用一次的閉包。所有閉包都是實現這個 trait,它會將捕獲的值移除閉包。FnMut
不會將捕獲的值移除閉包,可能會修改值。會被調用 多次。Fn
不會移除捕獲的值,也不修改捕獲的值。會被調用多次而不改變其環境。
這是Option<T>
的unwrap_or_else()
方法定義
impl<T> Option<T> {
pub fn unwrap_or_else<F>(self, f: F) -> T
where
F: FnOnce() -> T
{
match self {
Some(x) => x,
None => f(),
}
}
}
F
就是閉包指定的類型,T
是返回值類型。FnOnce()->T
表明瞭閉包會被調用一次,有值時Some
,返回值;沒有值時None
,f
調用一次。
在使用閉包時,如果我們不需要捕獲其環境中的值,則可以不使用閉包,而使用傳遞函數作為參數。
迭代器
迭代器是處理元素序列的方式。遍歷序列中的每一項以及決定序列何時結束的邏輯。
fn main(){
let arr = [1, 2, 3, 4];
for val in arr {
println!("{}", val)
}
}
迭代器都定義了Iterator
trait,並實現next
方法。調用next
返回迭代器的一個項,封裝在Some
中,結束後返回None
pub trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
}
type Item
和Self::Item
定義了 trait 的關聯類型。表明瞭迭代器返回值類型為Item
可以通過next()
方法迭代獲取值:
fn main(){
let arr = [1, 2, 3, 4];
let mut iter = arr.iter();
println!("{:?}", iter.next());
println!("{:?}", iter.next());
}
iter()
生成一個不可變引用的迭代器。對於迭代器實例iter
必須是mut
可變的。
into_ter()
獲取到 arr 所有權的迭代器。iter_mut()
可以獲取到可變引用迭代器。
消費適配器
調用next()
方法的方法被稱為消費適配器。
fn main() {
let arr = [1, 2, 3, 4];
let total: i8 = arr.iter().sum();
println!("{}", total);
}
這些方法總是會獲取迭代器的所有權並反覆調用 next
來遍歷迭代器。sum()
方法返回調用next
方法獲取值,最終返回和值。
迭代器適配器
將當前迭代器變為不同類型的迭代器。可以鏈式調用多個迭代器適配器,但是每次調用都必須調用消費適配器來獲取調用結果。
fn main(){
let arr = [1, 2, 3, 4];
let arr2: Vec<_> = arr.iter().map(|val| val + 1).collect();
for val in arr2 {
println!("{}", val)
}
}
map()
方法接受一個閉包函數,可以在遍歷元素上執行任何操作。進行了一次迭代適配器操作,然後通過collect()
方法獲取調用的結果值。
智能指針
指針是一個包含記憶體地址的變數。智能指針是一類數據結構,表現同指針,並擁有額外的元數據和功能。
智能指針通常使用結構體實現,實現了Deref
和Drop
trait。deref trait
允許智能指針結構體實例表現的像引用一樣;drop trait
允許智能指針離開作用域時自定義運行代碼
標準庫中常用的智能指針:
Box<T>
用於在堆上分配值Rc<T>
引用計數類型,其數據可以有多個所有者Ref<T>、RefMut<T>
通過RefCell<T>
訪問,這是一個在運行時執行借用規則的類型。
Box<T>
智能指針 box 允許將一個值放在堆上而不是棧上。留在棧上的則是指向堆數據的指針。
在以下情況下可以考慮使用:
- 編譯時未知大小的類型,又想在確切大小的上下文中使用這個類型的值。
- 當有大量數據不被拷貝的情況下轉移所有權的時候
- 當有一個值只關心它的類型是否實現特定 trait,而不是具體類型的時候
fn main(){
let b = Box::new(100);
println!("{}", b);
}
直接聲明創建 box 類型變數,並分配了一個值100
存儲在堆上, 可以直接訪問變數訪問值。
通過cons
list 數據結構定義遞歸數據類型
它是construct function
的縮寫,利用兩個參數構造一個新的列表.最後一項值包含了Nil
值,標識結束
enum List {
Cons(i32, Box<List>),
Nil,
}
use crate::List::{Cons, Nil};
fn main(){
let list = Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Nil))))));
}
Cons
可能會無限嵌套下去,為了保證 rust 編譯時計算需要的大小,只能通過Box
來幫助 rust 計算出List
需要的大小。
Deref
trait 重載解引用運算符*
之前已經使用過*
解引用值,可以獲取到指針指向引用的值。
fn main(){
let mut s = String::from("hboot");
let s1 = &mut s;
*s1 += " admin";
println!("{}", s)
}
s1
是 s 的可變引用,再通過*
解引用後,可以修改存儲在堆上的數據。
也可以通過Box<T>
代替引用,和*
擁有相同的功能。
fn main(){
let s = String::from("hboot");
let mut s1 = Box::new(s);
*s1 += " admin";
println!("{:?}", s1);
}
Box
會拷貝s
在棧上的指針數據,導致存儲在堆上的數據所有權被轉移,s
在後續變的不可用。
自定義實現一個智能指針MyBox
,它可以做到上面的解引用操作
#[derive(Debug)]
struct MyBox<T>(T);
impl<T> MyBox<T> {
fn new(val: T) -> MyBox<T> {
MyBox(val)
}
}
實現了一個元組結構體,自定義實例new
方法,接受一個參數進行初始化操作。還需要實現解引用功能,Deref
trait 由標準庫提供,實現 deref 方法
use std::ops::Deref;
impl<T> Deref for MyBox<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.0
}
}
上述的解引用例子,則可以由MyBox
代替實現。type Target = T
定義了 trait 的關聯類型,&self.0
訪問元組結構體的第一個元素。
fn main(){
let s = String::from("hboot");
let s1 = MyBox::new(s);
// *s1 += " admin";
println!("{:?}", *s1);
}
因為實現的是Deref
所以不能修改,修改時需要實現DerefMut
trait。
實現了Deref
trait 的數據類型,在函數傳參時,可做到隱式轉換,而不需要手動去轉換為參數需要的類型。
fn print(val: &str) {
println!("{}", val)
}
fn main(){
// 輸出上面的示例 s1
print(&s1);
}
對於數據的強制轉換,只能將可變引用轉為不可變引用;不能將不可變引用轉為可變引用。
Drop
trait 運行清理代碼
實現了Drop
trait 的數據,在離開作用域時,會調用其實現的drop
方法,它獲取一個可變引用。
為上述的MyBox
實現Drop
,無需引入,Drop
trait 是 prelude 的。
impl<T> Drop for MyBox<T> {
fn drop(&mut self) {
println!("mybox drop value");
}
}
再次調用執行,可以看到最終在程式執行完畢後,列印輸出了mybox drop value
. drop
會自動執行,而無需手動調用。
如果想要提前銷毀資源,則需要std::mem::drop
,可以調用drop
方法
fn main(){
drop(s1);
// 手動清理後,後續不能再使用s1
// print(&s1);
}
Rc<T>
引用計數啟用多所有權模式
在圖形結構中,每個節點都有多個邊指向,所以每個節點都會擁有指向它的邊的所有權。
通過使用Rc<T>
類型,記錄被引用的數量,來確定這個值有沒有被引用。如果為 0 沒有被引用,則會被清理。
Rc<T>
只適用於單線程
創建Rc
類型的變數s
,然後通過Rc::clone
克隆變數s
生成s1\s2
.
use std::rc::Rc;
fn main(){
let s = Rc::new(String::from("hboot"));
let s1 = Rc::clone(&s);
let s2 = Rc::clone(&s);
println!("s:{},s1:{},s2:{}", s, s1, s2)
}
這裡可以看到s1、s2
沒有獲取s
的所有權,它們仍然同時生效。Rc::clone
不同於深拷貝,只會增加引用計數。
可以通過strong_count()
方法查看被引用次數
fn main(){
let s = Rc::new(String::from("hboot"));
println!("s create - {}", Rc::strong_count(&s));
let s1 = Rc::clone(&s);
println!("s1 create - {}", Rc::strong_count(&s));
{
let s2 = Rc::clone(&s);
println!("s2 create - {}", Rc::strong_count(&s));
}
println!("s2 goes out of scope - {}", Rc::strong_count(&s));
}
執行測試輸出為
通過不可變引用,Rc<T>
允許程式在多個部分之間只讀地共用數據。
RefCell<T>
允許修改不可變引用
根據 rust 的不可變引用規則,被引用的變數是不允許修改。但是在某些模式下,可以做到修改,也就是內部可變性模式。
內部可變性通過在數據結構中使用unsafe
代碼來模糊 rust 的不可變性和借用規則。unsafe
不安全代碼表明我們需要手動去檢查代碼而不是讓編譯器檢查。
RefCell<T>
類型是在代碼運行時作用檢測不可變或可變借用規則,而通常的規則檢測是在編譯階段。
特點:
- 可以在允許出現特定記憶體安全的場景中使用。
- 需要確認你的代碼遵守了借用規則,但是 rust 無法理解
- 只能用於單線程
RefCell<T>
在運行時記錄借用,通過borrow()
和borrow_mut()
方法,會返回Ref<T>
和RefMut<T>
智能指針,並實現了Deref
trait.
定義一個MixName
trait,然後結構體User
實現了它,並實現它的方法mix
.
use std::cell::RefCell;
pub trait MixName {
fn mix(&self, suffix: &str);
}
struct User {
name: RefCell<String>,
}
impl User {
fn new() -> User {
User {
name: RefCell::new(String::from("hboot")),
}
}
}
impl MixName for User {
fn mix(&self, suffix: &str) {
self.name.borrow_mut().push_str(suffix);
}
}
mix
方法修改了 self 內部屬性name
的值,但是我們可以看到&self
時不可變引用,這歸功於RefCell<T>
創建值,使得不可變借用可以修改其內部值。
fn main(){
let user = User::new();
user.mix(" hello");
println!("{:?}", user.name.borrow());
}
執行程式可以看到內部的值已經被修改了。RefCell<T>
會在調用borrow
時,記錄借用次數,當離開了作用域時,借用次數減一。
RefCell<T>
只能有一個所有者,結合Rc<T>
使其擁有多個可變數據所有者。
use std::cell::RefCell;
use std::rc::Rc;
fn main(){
let s = Rc::new(RefCell::new(String::from("hboot")));
let s1 = Rc::clone(&s);
let s2 = Rc::clone(&s);
*s.borrow_mut() += " good";
println!("{:?}", s);
}
通過RefCell
來創建變數,然後通過Rc
開啟多所有權,這樣在*s.borrow_mut() += " good";
,修改後,變數s、s1、s2
的值都發生了變更。
但是這隻能在單線中使用,如果想要多線程使用,則需要使用併發安全的Mutex<T>
類型。
無畏併發
併發編程 - 代表程式的不同部分相互獨立的運行。
並行編程 - 代表程式不同部分同時執行。
thread
多線程運行代碼
多線程運行代碼可以提高程式的執行效率。也會造成一些問題
- 多個線程在不同時刻訪問同一數據資源,形成競爭
- 相互等待對方,造成死鎖
- 一些情況下出現的難以修複的 bug
使用thread::spawn
創建一個線程,它接受一個閉包函數
use std::thread;
fn main() {
thread::spawn(|| {
println!("hello!");
});
println!("rust!");
}
可以看到輸出,先是rust!
,也就是主線程先執行。可以多次執行cargo run
以觀察結果,會出現新線程沒有列印輸出,這是因為主線程結束,新線程也會結束,而不會等待新線程是否執行完畢。
可以通過線程休眠,展示這一特點
use std::thread;
use std::time::Duration;
fn main() {
thread::spawn(|| {
thread::sleep(Duration::from_millis(2));
println!("hello!");
});
println!("rust!");
}
程式基本沒有什麼機會切換到新線程去執行,也看不到新線程的列印輸出。
可以通過thread::spawn
的返回值線程實例,然後調用join()
方法,來等待線程結束
let thread = thread::spawn(|| {
thread::sleep(Duration::from_millis(2));
println!("hello!");
});
println!("rust!");
thread.join().unwrap();
再次執行,可以看到新線程的列印輸出。join()
會阻塞當前線程,知道線程實例thread
執行完畢。
可以將thread.join().unwrap();
放在主線程輸出之前,優先執行
thread.join().unwrap();
println!("rust!");
通過move
關鍵字強制閉包獲取其所有權,thread::spawn
創建線程給的閉包函數沒有任何參數,需要使用主線程里的變數
let name = String::from("hboot");
let thread = thread::spawn(move || {
thread::sleep(Duration::from_millis(2));
println!("hello! - {}", name);
});
新線程強制獲取了環境中變數的所有權,保證了新線程執行不會出錯。如果是引用,那麼由於新線程的執行順序,可能會在主線程執行過程使引用失效,從而導致新線程執行報錯
線程間消息傳遞
通過channel
實現線程間消息的傳遞併發。
通過mpsc::channel
創建通信通道,這個通道可以有多個發送端,但只能有一個接收端.
use std::sync::mpsc;
fn main(){
let (send, receive) = mpsc::channel();
thread::spawn(move || {
let name = String::from("rust");
send.send(name).unwrap();
});
let receive_str = receive.recv().unwrap();
println!("get thread msg :{}", receive_str);
}
mpsc::channel()
生成一個通過,返回一個元組,第一個是發送者,第二個是接收者。然後創建一個新線程,通過實例對象send
發送一條信息;在主線程中通過實例對象receive
接受數據。
不管是send()
發送方法還是recv()
方法,它們都返回Result<T,E>
類型,如果接受端或發送端被清除了,則會返回錯誤。
接受recv()
方法是阻塞線程的,也就是必須接收到一個值。還有一個方法try_recv()
方法則不會阻塞,需要頻繁去調用,在有可用消息時進行處理。
新線程將變數name
發送出去,那麼它的所有權也被轉移 出去了,後續不能使用它
send.send(name).unwrap();
// 在發送後,不能再使用改變數
println!("{}", name);
當在子線程中連續多次發送多個值時,可以通過迭代器遍歷receive
獲取值
fn main(){
let (send, receive) = mpsc::channel();
thread::spawn(move || {
send.send(1).unwrap();
send.send(10).unwrap();
send.send(100).unwrap();
});
for receive_str in receive {
println!("{}", receive_str);
}
}
上述例子只是單發送者,可以通過clone()
方法克隆send
發送對象,然後傳給另一個線程
fn main(){
let (send, receive) = mpsc::channel();
let send_str = send.clone();
thread::spawn(move || {
send_str.send("hello").unwrap();
send_str.send("rust").unwrap();
});
thread::spawn(move || {
send.send("good").unwrap();
send.send("hboot").unwrap();
});
for receive_str in receive {
println!("{}", receive_str);
}
}
創建兩個線程,一個線程傳入時克隆的send_str
,它們都發送消息,然後在主線程中,接收到所有消息。
多個線程由於執行順序導致列印輸出的順序也不盡相同。這依賴於系統,我們可以通過線程休眠做實驗,觀察到輸出的順序不同
線程間共用狀態
除了相互之間發送消息外, 還可以通過共用數據,來傳遞數據狀態變化。
通過Mutex<T>
創建共用數據,在需要使用的線程中通過lock()
獲取鎖,以訪問數據。
use std::sync::{Mutex};
fn main()[
let name = Mutex::new(String::from("hboot"));
{
let mut name = name.lock().unwrap();
*name += " good!";
}
println!("{:?}", name.lock().unwrap());
]
新創建的數據hboot
,在局部作用域中獲取鎖,然後解引用後變更值,最終列印輸出可以看到變更後的數據。
Mutext<T>
是一個智能指針,調用lock()
返回了一個MutexGuard
智能指針,它實現了Deref
來指向內部數據,同時也提供Drop
實現了當離開作用域時自動釋放鎖。
正因為這樣,我們在編碼時,不會因為忘記釋放鎖而導致其他線程訪問不了數據。
如果想要在多個線程中訪問共用數據,因為線程需要轉移所有權,這樣導致共用數據每次只能在一個線程中使用,通過Arc<T>
來創建多所有者,使得共用數據可被多個線程同時訪問。
use std::sync::{Arc, Mutex};
use std::thread;
fn main(){
let name = Arc::new(Mutex::new(String::from("hboot")));
let mut thread_arr = vec![];
for val in ["admin", "test", "hello", "rust"] {
let name = Arc::clone(&name);
let thread = thread::spawn(move || {
let mut name = name.lock().unwrap();
*name += val;
});
thread_arr.push(thread);
}
for thread in thread_arr {
thread.join().unwrap();
}
println!("{:?}", name.lock().unwrap())
}
Arc<T>
擁有和Rc<T>
相同的 api,它可以用於併發環境的類型。這是一個原子引用計數類型。
Mutex<T>
同RefCell<T>
一樣,提供了內部可變性,通過獲取內布值的可變引用修改值。當然,Mutex<T>
也會有出現相互引用鎖死的風險,兩個線程需要鎖住兩個資源而各自已經鎖了一個,造成了互相等待的問題。
Sync
和Send trait
擴展併發
除了使用 rust 標準庫提供的處理併發問題,還可以使用別人編寫的併發功能
當嘗試編寫併發功能時,有兩個併發概念:
-
通過
Send trait
表明實現了Send
的類型值的所有權可以線上程間傳遞。rust 幾乎所有類型都是Send
, 還有一些不能Send
,比如Rc<T>
,它只能用於單線程, -
通過
Sync trait
表明實現了Sync
的類型可以安全的在多個線程中擁有其值的引用。Rc<T>、RefCell<T>
都不是Sync
類型的。
根據這兩個概念,可以手動創建用於併發功能的併發類型,在使用時需要多加小心,以維護其安全保證。
追逐的不應該是夢想,隨心所欲,隨遇而安!