rust 自動化測試、迭代器與閉包、智能指針、無畏併發

来源:https://www.cnblogs.com/dreamHot/archive/2023/07/09/17538735.html
-Advertisement-
Play Games

編寫測試可以讓我們的代碼在後續迭代過程中不出現功能性缺陷問題;理解迭代器、閉包的函數式編程特性;`Box ...


編寫測試可以讓我們的代碼在後續迭代過程中不出現功能性缺陷問題;理解迭代器、閉包的函數式編程特性;Box<T>智能指針在堆上存儲數據,Rc<T>智能指針開啟多所有權模式等;理解併發,如何安全的使用線程,共用數據。

自動化測試

編寫測試以方便我們在後續的迭代過程中,不會改壞代碼。保證了程式的健壯性。

測試函數通常進行如下操作:

  1. 設置需要的數據或狀態
  2. 運行需要測試的代碼
  3. 斷言其結果是我們期望的

在 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就會看到執行完測試的詳細信息。包括了測試數量、通過測試數、失敗測試數等等維度

test.png

首先使用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斷言失敗時,會列印出兩個值,便於觀察為什麼失敗。因為會列印輸出,所以兩個值必須實現PartialEqDebug 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,可以看到測試示例通過了。如果我們符合參數要求,測試示例就會是失敗

test-should-panic.png

但如果我們代碼中有多個錯誤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在測試模式下編譯代碼併發運行生成的測試二進位文件。

  1. 可以通過設置測試線程,單次只執行一個測試示例
$> cargo test -- --test-threads=1

測試線程為 1,程式不會使用任何並行機制。

  1. 預設的測試在測試示例通過時,不會列印輸出。通過設置在測試成功時也輸出程式中的列印
$> cargo test -- --show-output
  1. 預設的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 是函數或結構體指定它們可以使用什麼類型的閉包。

從閉包如何任何處理值、閉包自動、漸進實現一個、多個 Fntrait

  • 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)
    }
}

迭代器都定義了Iteratortrait,並實現next方法。調用next返回迭代器的一個項,封裝在Some中,結束後返回None

pub trait Iterator {
    type Item;

    fn next(&mut self) -> Option<Self::Item>;
}

type ItemSelf::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()方法獲取調用的結果值。

智能指針

指針是一個包含記憶體地址的變數。智能指針是一類數據結構,表現同指針,並擁有額外的元數據和功能。

智能指針通常使用結構體實現,實現了DerefDroptrait。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需要的大小。

Dereftrait 重載解引用運算符*

之前已經使用過*解引用值,可以獲取到指針指向引用的值。

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方法,接受一個參數進行初始化操作。還需要實現解引用功能,Dereftrait 由標準庫提供,實現 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所以不能修改,修改時需要實現DerefMuttrait。

實現了Dereftrait 的數據類型,在函數傳參時,可做到隱式轉換,而不需要手動去轉換為參數需要的類型。

fn print(val: &str) {
    println!("{}", val)
}

fn main(){
    // 輸出上面的示例 s1
    print(&s1);
}

對於數據的強制轉換,只能將可變引用轉為不可變引用;不能將不可變引用轉為可變引用。

Drop trait 運行清理代碼

實現了Droptrait 的數據,在離開作用域時,會調用其實現的drop方法,它獲取一個可變引用。

為上述的MyBox實現Drop,無需引入,Droptrait 是 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-count.png

通過不可變引用,Rc<T>允許程式在多個部分之間只讀地共用數據。

RefCell<T> 允許修改不可變引用

根據 rust 的不可變引用規則,被引用的變數是不允許修改。但是在某些模式下,可以做到修改,也就是內部可變性模式。

內部可變性通過在數據結構中使用unsafe代碼來模糊 rust 的不可變性和借用規則。unsafe不安全代碼表明我們需要手動去檢查代碼而不是讓編譯器檢查。

RefCell<T>類型是在代碼運行時作用檢測不可變或可變借用規則,而通常的規則檢測是在編譯階段。

特點:

  • 可以在允許出現特定記憶體安全的場景中使用。
  • 需要確認你的代碼遵守了借用規則,但是 rust 無法理解
  • 只能用於單線程

RefCell<T> 在運行時記錄借用,通過borrow()borrow_mut()方法,會返回Ref<T>RefMut<T>智能指針,並實現了Dereftrait.

定義一個MixNametrait,然後結構體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>也會有出現相互引用鎖死的風險,兩個線程需要鎖住兩個資源而各自已經鎖了一個,造成了互相等待的問題。

SyncSend trait擴展併發

除了使用 rust 標準庫提供的處理併發問題,還可以使用別人編寫的併發功能

當嘗試編寫併發功能時,有兩個併發概念:

  • 通過Send trait表明實現了Send的類型值的所有權可以線上程間傳遞。rust 幾乎所有類型都是Send, 還有一些不能Send,比如Rc<T>,它只能用於單線程,

  • 通過Sync trait 表明實現了Sync的類型可以安全的在多個線程中擁有其值的引用。Rc<T>、RefCell<T>都不是Sync類型的。

根據這兩個概念,可以手動創建用於併發功能的併發類型,在使用時需要多加小心,以維護其安全保證。

追逐的不應該是夢想,隨心所欲,隨遇而安!
您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • # Qt deleteLater作用及源碼分析 > 個人經驗總結,如有錯誤或遺漏,歡迎各位大佬指正 🥳 在本篇文章中,我們將深入分析源碼,探討`deleteLater`的原理。 `deleteLater`是Qt框架提供的一個重要函數,用於在事件迴圈中延遲刪除對象。 在軟體開發中,延遲刪除對象的概念 ...
  • 如果模板里需要用變數填充表格,建議模板里的表格像word文件一樣建一個兩行的表格。但是這樣是freemaker是無法成功替換變數的,所以需要手動處理成到一個段里(如圖2),關於這點實在太無語了,因為沒有找到比較好的處理辦法,只能手工處理,在實際的開發工作中曾經花了幾個小時來做這件事情。根據模板文件生... ...
  • 一. 介紹 String、StringBuffer、StringBuilder: 前言: String、StringBuffer、StringBuilder 均在java.lang包下; String: 在Java中,String是一個特殊的引用類型,用於表示文本字元串。它提供了許多方法來操作和處理 ...
  • ## **Docker是什麼?** Docker是一個開源平臺,通過將應用程式隔離到輕量級、可移植的容器中,自動化應用程式的部署、擴展和管理。容器是獨立的可執行單元,封裝了運行應用程式所需的所有必要依賴項、庫和配置文件,可以在各種環境中穩定地運行。 # **什麼是容器?** 容器是一種輕量級、可移植 ...
  • - 背景: 經常創建和銷毀,使用量特別大的資源,比如併發情況下的線程,對性能影響很大。 - 思路:提前創建好多個線程,放入線程池中,使用時直接獲取,使用完放後池中。可以避免頻繁創建銷毀,實現重覆利用。類似生活中公共交通工具。 - 好處: - 提高響應速度(減少了創建新線程的時間) - 降低資源消耗( ...
  • **本文深入探討了 Django 中的請求與響應處理,從 Django 請求和響應的基礎知識、生命周期,到 HttpRequest 和 HttpResponse 對象的詳細介紹。同時,討論了 Django 的視圖和請求、響應處理,以及安全性和非同步處理的考慮。最後,對比了 Django 與 Flask ...
  • ### 三種創建方式 - Thread class 繼承Thread類 - Runnable介面 實現Runnable介面 - Callable介面 實現Callable介面 ### Thread - 自定義線程類繼承Thread類 - 重寫run()方法,編寫線程執行體 - 創建線程對象,調用st ...
  • python打包Windows.exe程式(pyinstaller) ## 基礎命令 `pip install pyinstaller` 使用pip命令來安裝pyinstaller模塊。 -F: `pyinstaller -F hello.py -p hello2.py` -D: `pyinstal ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...