同步指的是線程之間的協(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ī)制上并無差異,大致有下面幾種:
std::thread::sleep
,std::thread::sleep_ms
,std::thread::park_timeout
,std::thread::park_timeout_ms
,還有一些類似的其他API,由于太多,詳細(xì)信息就請參見官網(wǎng)std::thread
。std::thread::yield_now
,詳細(xì)信息參見官網(wǎng)std::thread
。std::thread::JoinHandle::join
,std::thread::park
,std::sync::Mutex::lock
等,還有一些同步相關(guān)的類的API也會導(dǎo)致線程等待。詳細(xì)信息參見官網(wǎng)std::thread
和std::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):
std::sync::Condvar::wait
和std::sync::Condvar::notify_one
,std::sync::Condvar::notify_all
。Condvar
之外,其實(shí)鎖也是具有自動通知功能的,當(dāng)持有鎖的線程釋放鎖的時(shí)候,等待鎖的線程就會自動被喚醒,以搶占鎖。關(guān)于鎖的介紹,在下面有詳解。std::sync::Barrier
。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也能編寫出非常安全的多線程程序。