鍍金池/ 教程/ Java/ 同步
標(biāo)準(zhǔn)輸入與輸出
消息傳遞
循環(huán)
注釋
Rust for Mac OS
幾種智能指針
Cell, RefCell
trait對象 (trait object)
rust web 開發(fā)
Unsafe、原始指針
Macro
迭代器
函數(shù)
Borrow, BorrowMut, ToOwned
快速上手
二叉樹
編輯器
測試與評測
Deref
安裝Rust
哈希表 HashMap
原生類型
17.錯誤處理
VS Code 安裝配置
動態(tài)數(shù)組Vec
模式匹配
操作符和格式化字符串
Rust for Linux
函數(shù)參數(shù)
Visual Studio
vim/GVim安裝配置
閉包作為參數(shù)和返回值
安全(Safety)
Cow
生命周期( Lifetime )
閉包的實(shí)現(xiàn)
所有權(quán)(Ownership)
Atom
將Rust編譯成庫
類型、運(yùn)算符和字符串
類型系統(tǒng)中的幾個常見 trait
特性
屬性和編譯器參數(shù)
Spacemacs
集合類型
Rust json處理
Heap & Stack
并行
標(biāo)準(zhǔn)庫示例
基本程序結(jié)構(gòu)
鏈表
trait 和 trait對象
前期準(zhǔn)備
代碼風(fēng)格
編譯器參數(shù)
基于語義化版本的項(xiàng)目版本聲明與管理
Rust 版本管理工具: rustup
引用&借用(References&Borrowing)
注釋與文檔
10.1 trait關(guān)鍵字
模式
調(diào)用ffi函數(shù)
unsafe
并發(fā),并行,多線程編程
AsRef 和 AsMut
Rust旅程
Rust for Windows
結(jié)構(gòu)體與枚舉
條件分支
附錄I-術(shù)語表
變量綁定與原生類型
Mutex 與 RwLock
泛型
裸指針
常用數(shù)據(jù)結(jié)構(gòu)實(shí)現(xiàn)
系統(tǒng)命令:調(diào)用grep
Into/From 及其在 String 和 &str 互轉(zhuǎn)上的應(yīng)用
共享內(nèi)存
Sublime
網(wǎng)絡(luò)模塊:W貓的回音
函數(shù)返回值
包和模塊
高階函數(shù)
函數(shù)與方法
match關(guān)鍵字
隊(duì)列
目錄操作:簡單grep
語句和表達(dá)式
并發(fā)編程
閉包
測試
閉包的語法
同步
迭代器
String
Send 和 Sync
Rc 和 Arc
屬性
Emacs
優(yōu)先隊(duì)列
Prelude
cargo簡介
控制流(control flow)
數(shù)組、動態(tài)數(shù)組和字符串
FFI
模塊和包系統(tǒng)、Prelude
實(shí)戰(zhàn)篇
Rust 是一門系統(tǒng)級編程語言,被設(shè)計(jì)為保證內(nèi)存和線程安全,并防止段錯誤。作為系統(tǒng)級編程語言,它的基本理念是 “零開銷抽象”。理
運(yùn)算符重載
Any和反射
rust數(shù)據(jù)庫操作
輸入輸出流
復(fù)合類型
性能測試

同步

同步

同步指的是線程之間的協(xié)作配合,以共同完成某個任務(wù)。在整個過程中,需要注意兩個關(guān)鍵點(diǎn):一是共享資源的訪問, 二是訪問資源的順序。通過前面的介紹,我們已經(jīng)知道了如何讓多個線程訪問共享資源,但并沒介紹如何控制訪問順序,才不會出現(xiàn)錯誤。如果兩個線程同時(shí)訪問同一內(nèi)存地址的數(shù)據(jù),一個寫,一個讀,如果不加控制,寫線程只寫了一半,讀線程就開始讀,必然讀到的數(shù)據(jù)是錯誤的,不可用的,從而造成程序錯誤,這就造成了并發(fā)安全問題,為此我們必須要有一套控制機(jī)制來避免這樣的事情發(fā)生。就好比兩個人喝一瓶可樂,只有一根吸管,那肯定也得商量出一個規(guī)則,才能相安無事地都喝到可樂。本節(jié)就將具體介紹在Rust中,我們要怎么做,才能解決這個問題。

繼續(xù)上面喝可樂的例子,一人一口的方式,就是一種解決方案,只要不是太笨,幾乎都能想到這個方案。具體實(shí)施時(shí),A在喝的時(shí)候,B一直在旁邊盯著,要是A喝完一口,B馬上拿過來喝,此時(shí)A肯定也是在旁邊盯著。在現(xiàn)實(shí)生活中,這樣的示例比比皆是。細(xì)想一下,貌似同步中都可能涉及到等待。諸葛先生在萬事具備,只欠東風(fēng)時(shí),也只能等,因?yàn)闂l件不成熟啊。依照這個邏輯,在操作系統(tǒng)和各大編程語言中,幾乎都支持當(dāng)前線程等待,當(dāng)然Rust也不例外。

等待

Rust中線程等待和其他語言在機(jī)制上并無差異,大致有下面幾種:

  • 等待一段時(shí)間后,再接著繼續(xù)執(zhí)行??雌饋砭拖褚粋€人工作累了,休息一會再工作。通過調(diào)用相關(guān)的API可以讓當(dāng)前線程暫停執(zhí)行進(jìn)入睡眠狀態(tài),此時(shí)調(diào)度器不會調(diào)度它執(zhí)行,等過一段時(shí)間后,線程自動進(jìn)入就緒狀態(tài),可以被調(diào)度執(zhí)行,繼續(xù)從之前睡眠時(shí)的地方執(zhí)行。對應(yīng)的API有std::thread::sleep,std::thread::sleep_ms,std::thread::park_timeout,std::thread::park_timeout_ms,還有一些類似的其他API,由于太多,詳細(xì)信息就請參見官網(wǎng)std::thread。
  • 這一種方式有點(diǎn)特殊,時(shí)間非常短,就一個時(shí)間片,當(dāng)前線程自己主動放棄當(dāng)前時(shí)間片的調(diào)度,讓調(diào)度器重新選擇線程來執(zhí)行,這樣就把運(yùn)行機(jī)會給了別的線程,但是要注意的是,如果別的線程沒有更好的理由執(zhí)行,當(dāng)然最后執(zhí)行機(jī)會還是它的。在實(shí)際的應(yīng)用業(yè)務(wù)中,比如生產(chǎn)者制造出一個產(chǎn)品后,可以放棄一個時(shí)間片,讓消費(fèi)者獲得執(zhí)行機(jī)會,從而快速地消費(fèi)才生產(chǎn)的產(chǎn)品。這樣的控制粒度非常小,需要合理使用,如果需要連續(xù)放棄多個時(shí)間片,可以借用循環(huán)實(shí)現(xiàn)。對應(yīng)的API是std::thread::yield_now,詳細(xì)信息參見官網(wǎng)std::thread。
  • 1和2的等待都無須其他線程的協(xié)助,即可在一段時(shí)間后繼續(xù)執(zhí)行。最后我們還遇到一種等待,是需要其他線程參與,才能把等待的線程叫醒,否則,線程會一直等待下去。好比一個女人,要是沒有遇到一個男人,就永遠(yuǎn)不可能擺脫單身的狀態(tài)。相關(guān)的API包括std::thread::JoinHandle::joinstd::thread::park,std::sync::Mutex::lock等,還有一些同步相關(guān)的類的API也會導(dǎo)致線程等待。詳細(xì)信息參見官網(wǎng)std::threadstd::sync。

第一種和第三種等待方式,其實(shí)我們在上面的介紹中,都已經(jīng)遇到過了,它們也是使用的最多的兩種方式。在此,也可以回過頭去看看前面的使用方式和使用效果,結(jié)合自己的理解,做一些簡單的練習(xí)。

毫無疑問,第三種方式稍顯復(fù)雜,要將等待的線程叫醒,必然基于一定的規(guī)則,比如早上7點(diǎn)必須起床,那么就定一個早上7點(diǎn)的鬧鐘,到時(shí)間了就響,沒到時(shí)間別響。不管基于什么規(guī)則,要觸發(fā)叫醒這個事件,就肯定是某個條件已經(jīng)達(dá)成了?;谶@樣的邏輯,在操作系統(tǒng)和編程語言中,引入了一種叫著條件變量的東西??梢阅M現(xiàn)實(shí)生活中的鬧鐘的行為,條件達(dá)成就通知等待條件的線程。Rust的條件變量就是std::sync::Condvar,詳情參見官網(wǎng)條件變量。但是通知也并不只是條件變量的專利,還有其他的方式也可以觸發(fā)通知,下面我們就來瞧一瞧。

通知

看是簡單的通知,在編程時(shí)也需要注意以下幾點(diǎn):

  • 通知必然是因?yàn)橛械却?,所以通知和等待幾乎都是成對出現(xiàn)的,比如std::sync::Condvar::waitstd::sync::Condvar::notify_one,std::sync::Condvar::notify_all。
  • 等待所使用的對象,與通知使用的對象是同一個對象,從而該對象需要在多個線程之間共享,參見下面的例子。
  • 除了Condvar之外,其實(shí)也是具有自動通知功能的,當(dāng)持有鎖的線程釋放鎖的時(shí)候,等待鎖的線程就會自動被喚醒,以搶占鎖。關(guān)于鎖的介紹,在下面有詳解。
  • 通過條件變量和鎖,還可以構(gòu)建更加復(fù)雜的自動通知方式,比如std::sync::Barrier。
  • 通知也可以是1:1的,也可以是1:N的,Condvar可以控制通知一個還是N個,而鎖則不能控制,只要釋放鎖,所有等待鎖的其他線程都會同時(shí)醒來,而不是只有最先等待的線程。

下面我們分析一個簡單的例子:

use std::sync::{Arc, Mutex, Condvar};
use std::thread;

fn main() {

    let pair = Arc::new((Mutex::new(false), Condvar::new()));
    let pair2 = pair.clone();

    // 創(chuàng)建一個新線程
    thread::spawn(move|| {
        let &(ref lock, ref cvar) = &*pair2;
        let mut started = lock.lock().unwrap();
        *started = true;
        cvar.notify_one();
        println!("notify main thread");
    });

    // 等待新線程先運(yùn)行
    let &(ref lock, ref cvar) = &*pair;
    let mut started = lock.lock().unwrap();
    while !*started {
        println!("before wait");
        started = cvar.wait(started).unwrap();
        println!("after wait");
    }
}

運(yùn)行結(jié)果:

before wait
notify main thread
after wait

這個例子展示了如何通過條件變量和鎖來控制新建線程和主線程的同步,讓主線程等待新建線程執(zhí)行后,才能繼續(xù)執(zhí)行。從結(jié)果來看,功能上是實(shí)現(xiàn)了。對于上面這個例子,還有下面幾點(diǎn)需要說明:

  • Mutex是Rust中的一種鎖。
  • Condvar需要和Mutex一同使用,因?yàn)橛?code>Mutex保護(hù),Condvar并發(fā)才是安全的。
  • Mutex::lock方法返回的是一個MutexGuard,在離開作用域的時(shí)候,自動銷毀,從而自動釋放鎖,從而避免鎖沒有釋放的問題。
  • Condvar在等待時(shí),時(shí)會釋放鎖的,被通知喚醒時(shí),會重新獲得鎖,從而保證并發(fā)安全。

到此,你應(yīng)該對鎖比較感興趣了,為什么需要鎖?鎖存在的目的就是為了保證資源在同一個時(shí)間,能有序地被訪問,而不會出現(xiàn)異常數(shù)據(jù)。但其實(shí)要做到這一點(diǎn),也并不是只有鎖,包括鎖在內(nèi),主要涉及兩種基本方式:

原子類型

原子類型是最簡單的控制共享資源訪問的一種機(jī)制,相比較于后面將介紹的鎖而言,原子類型不需要開發(fā)者處理加鎖和釋放鎖的問題,同時(shí)支持修改,讀取等操作,還具備較高的并發(fā)性能,從硬件到操作系統(tǒng),到各個語言,基本都支持。在標(biāo)準(zhǔn)庫std::sync::atomic中,你將在里面看到Rust現(xiàn)有的原子類型,包括AtomicBool,AtomicIsize,AtomicPtr,AtomicUsize。這4個原子類型基本能滿足百分之九十的共享資源安全訪問的需要。下面我們就用原子類型,結(jié)合共享內(nèi)存的知識,來展示一下一個線程修改,一個線程讀取的情況:

use std::thread;
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};

fn main() {
    let var : Arc<AtomicUsize> = Arc::new(AtomicUsize::new(5));
    let share_var = var.clone();

    // 創(chuàng)建一個新線程
    let new_thread = thread::spawn(move|| {
        println!("share value in new thread: {}", share_var.load(Ordering::SeqCst));
        // 修改值
        share_var.store(9, Ordering::SeqCst);
    });

    // 等待新建線程先執(zhí)行
    new_thread.join().unwrap();
    println!("share value in main thread: {}", var.load(Ordering::SeqCst));
}

運(yùn)行結(jié)果:

share value in new thread: 5
share value in main thread: 9

結(jié)果表明新建線程成功的修改了值,并在主線程中獲取到了最新值,你也可以嘗試使用其他的原子類型。此處我們可以思考一下,如果我們用Arc::new(*mut Box<u32>)是否也可以做到? 為什么? 思考后,大家將體會到Rust在多線程安全方面做的有多么的好。除了原子類型,我們還可以使用鎖來實(shí)現(xiàn)同樣的功能。

在多線程中共享資源,除了原子類型之外,還可以考慮用鎖來實(shí)現(xiàn)。在操作之前必須先獲得鎖,一把鎖同時(shí)只能給一個線程,這樣能保證同一時(shí)間只有一個線程能操作共享資源,操作完成后,再釋放鎖給等待的其他線程。在Rust中std::sync::Mutex就是一種鎖。下面我們用Mutex來實(shí)現(xiàn)一下上面的原子類型的例子:

use std::thread;
use std::sync::{Arc, Mutex};

fn main() {
    let var : Arc<Mutex<u32>> = Arc::new(Mutex::new(5));
    let share_var = var.clone();

    // 創(chuàng)建一個新線程
    let new_thread = thread::spawn(move|| {
        let mut val = share_var.lock().unwrap();
        println!("share value in new thread: {}", *val);
        // 修改值
        *val = 9;
    });

    // 等待新建線程先執(zhí)行
    new_thread.join().unwrap();
    println!("share value in main thread: {}", *(var.lock().unwrap()));
}

運(yùn)行結(jié)果:

share value in new thread: 5
share value in main thread: 9

結(jié)果都一樣,看來用Mutex也能實(shí)現(xiàn),但如果從效率上比較,原子類型會更勝一籌。暫且不論這點(diǎn),我們從代碼里面看到,雖然有lock,但是并么有看到有類似于unlock的代碼出現(xiàn),并不是不需要釋放鎖,而是Rust為了提高安全性,已然在val銷毀的時(shí)候,自動釋放鎖了。同時(shí)我們發(fā)現(xiàn),為了修改共享的值,開發(fā)者必須要調(diào)用lock才行,這樣就又解決了一個安全問題。不得不再次贊嘆一下Rust在多線程方面的安全性做得真是太好了。如果是其他語言,我們要做到安全,必然得自己來實(shí)現(xiàn)這些。

為了保障鎖使用的安全性問題,Rust做了很多工作,但從效率來看還不如原子類型,那么鎖是否就沒有存在的價(jià)值了?顯然事實(shí)不可能是這樣的,既然存在,那必然有其價(jià)值。它能解決原子類型鎖不能解決的那百分之十的問題。我們再來看一下之前的一個例子:

use std::sync::{Arc, Mutex, Condvar};
use std::thread;

fn main() {

    let pair = Arc::new((Mutex::new(false), Condvar::new()));
    let pair2 = pair.clone();

    // 創(chuàng)建一個新線程
    thread::spawn(move|| {
        let &(ref lock, ref cvar) = &*pair2;
        let mut started = lock.lock().unwrap();
        *started = true;
        cvar.notify_one();
        println!("notify main thread");
    });

    // 等待新線程先運(yùn)行
    let &(ref lock, ref cvar) = &*pair;
    let mut started = lock.lock().unwrap();
    while !*started {
        println!("before wait");
        started = cvar.wait(started).unwrap();
        println!("after wait");
    }
}

代碼中的Condvar就是條件變量,它提供了wait方法可以主動讓當(dāng)前線程等待,同時(shí)提供了notify_one方法,讓其他線程喚醒正在等待的線程。這樣就能完美實(shí)現(xiàn)順序控制了??雌饋砗孟駰l件變量把事都做完了,要Mutex干嘛呢?為了防止多個線程同時(shí)執(zhí)行條件變量的wait操作,因?yàn)闂l件變量本身也是需要被保護(hù)的,這就是鎖能做,而原子類型做不到的地方。

在Rust中,Mutex是一種獨(dú)占鎖,同一時(shí)間只有一個線程能持有這個鎖。這種鎖會導(dǎo)致所有線程串行起來,這樣雖然保證了安全,但效率并不高。對于寫少讀多的情況來說,如果在沒有寫的情況下,都是讀取,那么應(yīng)該是可以并發(fā)執(zhí)行的,為了達(dá)到這個目的,幾乎所有的編程語言都提供了一種叫讀寫鎖的機(jī)制,Rust中也存在,叫std::sync::RwLock,在使用上同Mutex差不多,在此就留給大家自行練習(xí)了。

同步是多線程編程的永恒主題,Rust已經(jīng)為我們提供了良好的編程范式,并強(qiáng)加檢查,即使你之前沒有怎么接觸過,用Rust也能編寫出非常安全的多線程程序。

上一篇:將Rust編譯成庫下一篇:Emacs