譯者 | 盧鑫旺
審校 | 云昭
將Rust比作C++的小弟的話,相信大家都不會(huì)有異議。Rust借鑒了許多C++的設(shè)計(jì)思想。并發(fā)特性亦是如此。
Rust標(biāo)準(zhǔn)庫的并發(fā)特性與C++ 11中的特性非常相似:線程、原子操作、鎖和互斥量、條件變量等等。然而,在過去的幾年中,隨著C++ 17和C++ 20發(fā)布,C++已經(jīng)獲得了相當(dāng)多新的與并發(fā)相關(guān)的特性,未來的版本還會(huì)有更多的可借鑒之處。
讓我們花點(diǎn)時(shí)間來回顧一下C++的并發(fā)特性,討論一下這些特性在Rust下會(huì)是什么樣子的,以及要達(dá)到這個(gè)效果需要做些什么。
atomic_ref
P0019R8引入了std::atomic_ref到C++ 中。它是一種允許你將非原子對(duì)象用作原子對(duì)象的類型。例如,你可以創(chuàng)建一個(gè)atomic_ref<int>,它引用一個(gè)常規(guī)的int類型的變量,這時(shí)你可以使用與原子類型atomic<int>相同的功能,就跟它是atomic<int>一樣。
在C++中,這需要一個(gè)復(fù)制大部分原子接口的全新類型,而等效的Rust特性是一行函數(shù):atomic*::from_mut。例如,該函數(shù)允許你將&mut u32轉(zhuǎn)換為&AtomicU32,這是一種在Rust中完全正確的別名形式。C++ atomic_ref類型附帶了需要手動(dòng)維護(hù)的安全要求。只要你使用atomic_ref來訪問對(duì)象,那么對(duì)該對(duì)象的所有訪問都必須通過atomic_ ref。當(dāng)仍然存在atomic_ref時(shí)直接訪問它會(huì)導(dǎo)致未定義的行為。然而,在Rust中,這已經(jīng)由借用檢查器完全處理。編譯器理解,通過可變地借用u32,在借用結(jié)束之前,不允許任何東西直接訪問該u32。進(jìn)入from_mut函數(shù)的&mut u32的生命周期將作為從中得到的&AtomicU32的一部分保留。你可以根據(jù)需要復(fù)制任意數(shù)量的&AtomicU32副本,但只有在該引用的所有副本都消失后,原始借用才會(huì)結(jié)束。
from_mut函數(shù)目前不太穩(wěn)定,但也許是時(shí)候穩(wěn)定它了。
泛型原子類型
在C++中,std::atomic是泛型的:你可以有一個(gè)atomic<int>,也可以有atomic<myownstuct>。另一方面,在Rust中,我們只有特定的原子類型:AtomicU32、AtomicBool、AtomicUsize等。
C++的原子類型支持任何大小的對(duì)象,無論平臺(tái)是否支持。對(duì)于平臺(tái)本機(jī)原子操作不支持的大小的對(duì)象,它會(huì)自動(dòng)返回到基于鎖的實(shí)現(xiàn)。Rust則只提供平臺(tái)本機(jī)支持的類型。如果你正在用沒有64位原子的平臺(tái)進(jìn)行編譯,則AtomicU64不存在。
這有優(yōu)點(diǎn)也有缺點(diǎn)。這意味著使用AtomicU64的Rust代碼可能無法在某些平臺(tái)上編譯,但也意味著當(dāng)某些類型默默地返回到一個(gè)非常不同的實(shí)現(xiàn)時(shí),不會(huì)出現(xiàn)與性能相關(guān)的意外。這也意味著我們可以假設(shè)一個(gè)AtomicU64與內(nèi)存中的u64完全相同,允許使用類似AtomicU64::from_mut的函數(shù)。在Rust中使用一個(gè)泛型原子類型atomic<T>來處理任何大小的類型可能會(huì)很棘手。沒有專門化,我們無法使automic<LargeThing>包含Mutex,而不將其包含在automic<SmallThing>中。然而,我們可以做的是將互斥量存儲(chǔ)在一個(gè)全局HashMap中,由內(nèi)存地址索引。然后,automic<T>的大小可以與T相同,并在必要時(shí)使用此全局HashMap中的互斥量。這就是流行的atomic所做的事情。在Rust標(biāo)準(zhǔn)庫中添加這樣一個(gè)通用的范型automic<T>類型的建議需要討論它是否應(yīng)該在no_std程序中使用。常規(guī)哈希映射需要分配,這在no_std程序中是不可能的。固定大小的表可能適用于no_std程序,但由于各種原因可能不受歡迎。
Compare-exchange與填充
P0528R3更改了compare_exchange處理填充的方式。atomic<TypeWithPadding>上的比較交換操作也用于比較填充位,但結(jié)果證明這是一個(gè)壞主意。如今,填充位不再包括在比較中。
由于Rust目前只為整數(shù)提供原子類型,沒有任何填充,因此此更改與Rust無關(guān)。然而,使用compare_exchange方法的atomic<T>方案需要討論如何處理填充,并且可能需要從該方案中獲取輸入。
Compare-exchange內(nèi)存排序
在C++11中,compare_exchange函數(shù)要求成功內(nèi)存排序至少與失敗排序一樣強(qiáng)。不接受compare_exchange(…,…,memory_order_release,memory_ order_ acquire)。該要求被逐字復(fù)制到Rust的compare_exchange函數(shù)中。P0418R2認(rèn)為應(yīng)取消此限制,這是C++17的一部分。作為Rust 1.64和Rust lang/Rust#98383的一部分,解除了相同的限制。
Constexpr互斥量構(gòu)造函數(shù)
C++的std::mutex有一個(gè)constexpr構(gòu)造函數(shù),這意味著它可以在編譯時(shí)作為常量求值的一部分進(jìn)行構(gòu)造。然而,并非所有的實(shí)現(xiàn)都真正提供了這一點(diǎn)。例如,微軟的std::mutex實(shí)現(xiàn)不包括constexpr構(gòu)造函數(shù)。因此,依賴這一點(diǎn)對(duì)于可移植代碼來說是個(gè)壞主意。
另外,有趣的是,C++的std:: condition_variable和std:: shared_mutex根本不提供constexpr構(gòu)造函數(shù)。在Rust 1.0中,Rust的原始互斥不包括常量fn new。再加上Rust對(duì)靜態(tài)初始化的嚴(yán)格要求,這使得在靜態(tài)變量中使用互斥非常煩人。這在Rust 1.63.0中作為Rust lang/Rust#93740的一部分得到了解決,所有:?
- Mutex:: new
- rBlock:: new
- Condvar:: new
現(xiàn)在都是常量函數(shù)。
Latches與barriers
P1135R6在C++20中引入了std::ltatch和std::barriers,這兩種類型都允許等待多個(gè)線程到達(dá)某一點(diǎn)。latch基本上只是一個(gè)計(jì)數(shù)器,它由每個(gè)線程遞減,并允許你等待它達(dá)到零。它只能使用一次。barrier是這種思想的更高級(jí)版本,可以重復(fù)使用,并接受計(jì)數(shù)器達(dá)到零時(shí)自動(dòng)執(zhí)行的“完成函數(shù)”。Rust從1.0開始就有了類似的barrier類型。它是受pthread(pthrea_Barrier_t)而不是C++的啟發(fā)。Rust的(和pthread的)barrier不如C++中現(xiàn)在包含的靈活。它只有一個(gè)“遞減和等待”操作(稱為等待),并且缺少C++的std::barrier附帶的“僅等待”、“僅遞減”和“遞減和刪除”函數(shù)。另一方面,與C++不同,Rust(和pthread)的“遞減和等待”操作將一個(gè)線程指定為組長。這是完成函數(shù)的一種(可能更靈活)替代方法。
Rust版本上缺失的操作可以在任何時(shí)候輕松添加。我們所需要的只是這些新方法的名稱的一個(gè)好建議。
信號(hào)量
同樣的,P1135R6還向C++20添加了信號(hào)量:
- std::counting_semaphore
- std::binary_semaphore
Rust沒有通用的信號(hào)量類型,盡管它確實(shí)通過thread::park和unpark為每個(gè)線程提供了有效的二進(jìn)制信號(hào)量。
使用Mutex<u32>和Condvar可以輕松地手動(dòng)構(gòu)建信號(hào)量,但大多數(shù)操作系統(tǒng)允許使用單個(gè)AtomicU32實(shí)現(xiàn)更高效、更小的實(shí)現(xiàn)。例如,通過Linux上的futex()和Windows上的waitoAddress()。可以用于這些操作的原子大小取決于操作系統(tǒng)及其版本。C++的counting_semaphore是一個(gè)模板,它以一個(gè)整數(shù)作為參數(shù)來指示我們希望能夠計(jì)數(shù)到什么程度。例如,counting_semaphore<1000>可以計(jì)數(shù)到至少1000,因此將是16位或更大。binary_semaphore類型只是counting_Sema phore<1>的別名,在某些平臺(tái)上可以是單個(gè)字節(jié)。在Rust中,我們可能還沒有很快為這種泛型類型做好準(zhǔn)備。Rust的泛型強(qiáng)制了某種一致性,這對(duì)我們可以將常量作為泛型參數(shù)進(jìn)行處理帶來了一些限制。
我們可以有單獨(dú)的信號(hào)量32、信號(hào)量64等等,但這似乎有點(diǎn)過分了。擁有信號(hào)量<u32>和信號(hào)量<u64>甚至信號(hào)量<bool>都是可能的,但這是我們以前在標(biāo)準(zhǔn)庫中沒有做過的事情。我們的原子類型簡單地是AtomicU32、AtomicU64等等。如上所述,對(duì)于我們的原子類型,我們只提供你正在編譯的平臺(tái)本機(jī)支持的類型。如果我們將同樣的理念應(yīng)用于信號(hào)量,它將不存在于沒有futex或WaitoAddress功能的平臺(tái)上,例如macOS。如果我們有不同大小的單獨(dú)信號(hào)量類型,某些大小在(某些版本的)Linux和各種BSD上是不存在的。如果我們想在Rust中使用標(biāo)準(zhǔn)信號(hào)量類型,我們首先需要一些輸入,說明我們是否確實(shí)需要不同大小的信號(hào)量,以及需要何種形式的靈活性和可移植性才能使它們有用。也許我們應(yīng)該只使用一種始終可用的32位信號(hào)量類型(使用基于鎖的回退),但任何此類建議都必須包括對(duì)用例和限制的詳細(xì)解釋。
原子等待和通知
P1135R6添加到C++20的其余新功能是原子等待和通知函數(shù)。
這些函數(shù)通過標(biāo)準(zhǔn)接口有效地直接公開Linux的futex()和Windows的waitoAddress()。
然而,無論操作系統(tǒng)支持什么,它們都可以在所有大小的原子上、所有平臺(tái)上使用。Linux Futex(在FUTEX2之前)始終是32位的,但C++也允許atomic<uint64_t>:wait。
一種方法是使用類似于“停車場”的東西:有效地將內(nèi)存地址映射到鎖和隊(duì)列的全局哈希映射。這意味著Linux上的32位等待操作可以使用非常快速的基于futex的實(shí)現(xiàn),而其他大小的操作將使用非常不同的實(shí)現(xiàn)。如果我們遵循只提供本機(jī)支持的類型和函數(shù)的理念(就像我們對(duì)原子類型所做的那樣),我們就不會(huì)提供這樣的回退實(shí)現(xiàn)。這意味著我們?cè)贚inux上只有AtomicU32::wait(和AtomicI32::wait),而在Windows上,所有的原子類型都包括這個(gè)wait方法。在Rust中使用Atomic*::wait和Atomic*::notify需要討論回退到全局表在Rust中是否合適。
jthread和stop_token
P0660R10將std::jthread和std::stop_token添加到了C ++20中。
如果我們暫時(shí)忽略stop_token,jthread基本上只是一個(gè)在銷毀時(shí)自動(dòng)獲取join()方法的的常規(guī)std::thread。這避免了意外地分離線程并使其運(yùn)行的時(shí)間比預(yù)期的長,這在常規(guī)線程中可能會(huì)發(fā)生。然而,它也引入了一個(gè)潛在的新陷阱:立即銷毀jthread對(duì)象將立即加入線程,有效地消除了任何潛在的并行性。從Rust 1.63.0開始,提供了范圍線程(Rust lang/Rust#93203)。與jthread一樣,作用域線程也會(huì)自動(dòng)加入。然而,它們的連接點(diǎn)是明確的,并且保證安全可靠。借用檢查器甚至可以理解這一保證,允許你安全地借用作用域線程中的局部變量,只要這些變量超出作用域。除了自動(dòng)加入之外,jthreads的一個(gè)主要特性是其stop_token和相應(yīng)的stop_ source??梢栽趕top_source上調(diào)用request_stop(),使stop_ token上相應(yīng)的stopUrequest()方法返回true。這可以很好地要求線程停止,并在加入之前在jthread的析構(gòu)函數(shù)中自動(dòng)完成。由線程的代碼來實(shí)際檢查令牌,并在設(shè)置時(shí)停止。到目前為止,它看起來幾乎像一個(gè)普通的AtomicBool。不同的是stop_callback類型。這種類型允許用停止令牌注冊(cè)回調(diào)函數(shù),即“停止函數(shù)”。使用相應(yīng)的停止源請(qǐng)求停止將執(zhí)行此功能。實(shí)際上,線程可以使用它來讓其他線程知道如何停止或取消其工作。
在Rust中,我們可以很容易地將類似atomicboolean的功能添加到thread:: Scope的Scope對(duì)象中。簡單的is_finished(&self) -> bool或stop_requested(&self) -> bool指示主作用域函數(shù)是否已完成可能就夠了。可以結(jié)合request_stop(&self)方法從任何地方請(qǐng)求它。
stop_callback特性更加復(fù)雜,任何Rust的等價(jià)功能都可能需要詳細(xì)的提議來討論它的接口、用例和限制。
原子浮點(diǎn)數(shù)
P0020R6在C++ 20中增加了對(duì)原子浮點(diǎn)加法和減法的支持。在Rust中添加AtomicF32或AtomicF64也很容易,但吊詭的是,似乎目前原生支持原子浮點(diǎn)運(yùn)算的平臺(tái)往往是GPU廠商,而Rust現(xiàn)在好像并沒有提供對(duì)這些平臺(tái)的支持。關(guān)于向Rust添加這些類型方面,強(qiáng)烈建議提供一些實(shí)用的用例。
字節(jié)原子內(nèi)存
目前,在Rust或C++中不可能有效地實(shí)現(xiàn)遵循內(nèi)存模型所有規(guī)則的序列鎖。
P1478R7建議在未來的C++版本中添加atomic_load_per_byte_memcpy和atomic_store_per_byte_memcpy來解決這個(gè)問題。
對(duì)于Rust,這里給出一個(gè)想法,就是可以通過AtomicPerByte<T>類型:RFC 3301來公開功能。
原子shared_ptr
P0718R2為C++20添加了atomic<shared_ptr>和atomic<weak_ptr>的專門化。
引用計(jì)數(shù)指針(C++中的shared_ptr,Rust中的Arc)通常用于并發(fā)無鎖數(shù)據(jù)結(jié)構(gòu)。通過正確處理引用計(jì)數(shù),原子<shared_ptr>專門化使正確執(zhí)行此操作更加容易。
在Rust中,我們可以添加等效的AtomicArc<T>和AtomicWeak<T>類型。(雖然AtomicArc聽起來有點(diǎn)奇怪,但考慮到Arc的A已經(jīng)代表“原子”了。)
然而,C++的shared_ptr<T>是可為空的,而在Rust中,它需要一個(gè)選項(xiàng)<Arc<T>。目前還不清楚AtomicArc<T>是否應(yīng)該為空,或者我們是否也應(yīng)該有一個(gè)AtomicOptionArc<T>。
流行的arc-swap已經(jīng)在Rust中提供了所有這些變體,但據(jù)我所知,目前還沒有任何類似于標(biāo)準(zhǔn)庫的建議。
synchronized_value
盡管P0290R2沒有被接受,但提出了一種稱為synchronized_value<T>的類型,它將互斥鎖與數(shù)據(jù)類型T組合在一起。盡管它當(dāng)時(shí)沒有被C++接受,但這是一個(gè)有趣的建議,因?yàn)閟ynchronize_value<T>與Rust中的Mutex<T>幾乎完全相同。
在C++中,std::mutex不包含它保護(hù)的數(shù)據(jù),甚至根本不知道它保護(hù)的是什么。這意味著,需要由用戶來記住哪些數(shù)據(jù)受保護(hù)以及由哪個(gè)互斥鎖保護(hù),并確保每次訪問“受保護(hù)”數(shù)據(jù)時(shí)鎖定正確的互斥鎖。Rust的Mutex設(shè)計(jì),使用了一個(gè)類似于(可變的)T引用的MutexGuard,這使得安全性更高,同時(shí)在只需要一個(gè)互斥鎖而不需要任何數(shù)據(jù)的情況下,仍然允許使用Mutex<()>。synchronized_value的提議試圖將此模式添加到C++中,但是使用閉包而不是互斥鎖,因?yàn)镃++不跟蹤生命期。
結(jié)語
在筆者看來,C++可以繼續(xù)成為Rust的靈感來源,盡管“直接復(fù)制粘貼”的想法并不值得提倡,但好的思想還是要學(xué)習(xí)和繼承的。正如我們看到的Mutex,作用域線程,Atomic*::from_mut等,在Rust中提供相同功能的同時(shí),事情往往會(huì)變得非常不同。
當(dāng)然,提供與C++完全相同的功能不應(yīng)該是主要目標(biāo)。目標(biāo)應(yīng)該是準(zhǔn)確地提供Rust生態(tài)系統(tǒng)從語言和標(biāo)準(zhǔn)庫中需要的東西,這可能與C++用戶從他們的語言中需要的東西不同。如果你有來自Rust標(biāo)準(zhǔn)庫的并發(fā)需求,而目前還沒有滿足,歡迎把它留在評(píng)論區(qū),不管它是否已經(jīng)用另一種語言解決了。
原文鏈接:
https://blog.m-ou.se/rust-cpp-concurrency/
譯者介紹
盧鑫旺,51CTO社區(qū)編輯,編程語言愛好者,對(duì)數(shù)據(jù)庫,架構(gòu),云原生有濃厚興趣,目前就職某跨境電商出海營銷公司,擔(dān)任后端開發(fā)工作。