您現在的位置是:首頁 > 藝術

對比C++併發庫,Rust簡直不要太像!

由 51CTO 發表于 藝術2022-10-17
簡介在C++中,這需要一個複製大部分原子介面的全新型別,而等效的Rust特性是一行函式:atomic*::from_mut

立flang什麼梗

譯者 | 盧鑫旺

將Rust比作C++的小弟的話,相信大家都不會有異議。Rust借鑑了許多C++的設計思想。併發特性亦是如此。

Rust標準庫的併發特性與C++ 11中的特性非常相似:執行緒、原子操作、鎖和互斥量、條件變數等等。然而,在過去的幾年中,隨著C++ 17和C++ 20釋出,C++已經獲得了相當多新的與併發相關的特性,未來的版本還會有更多的可借鑑之處。

讓我們花點時間來回顧一下C++的併發特性,討論一下這些特性在Rust下會是什麼樣子的,以及要達到這個效果需要做些什麼。

對比C++併發庫,Rust簡直不要太像!

atomic_ref

P0019R8引入了std::atomic_ref到C++ 中。它是一種允許你將非原子物件用作原子物件的型別。例如,你可以建立一個atomic_ref,它引用一個常規的int型別的變數,這時你可以使用與原子型別atomic相同的功能,就跟它是atomic一樣。

在C++中,這需要一個複製大部分原子介面的全新型別,而等效的Rust特性是一行函式:atomic*::from_mut。例如,該函式允許你將&mut u32轉換為&AtomicU32,這是一種在Rust中完全正確的別名形式。C++ atomic_ref型別附帶了需要手動維護的安全要求。只要你使用atomic_ref來訪問物件,那麼對該物件的所有訪問都必須透過atomic_ ref。當仍然存在atomic_ref時直接訪問它會導致未定義的行為。然而,在Rust中,這已經由借用檢查器完全處理。編譯器理解,透過可變地借用u32,在借用結束之前,不允許任何東西直接訪問該u32。進入from_mut函式的&mut u32的生命週期將作為從中得到的&AtomicU32的一部分保留。你可以根據需要複製任意數量的&AtomicU32副本,但只有在該引用的所有副本都消失後,原始借用才會結束。

from_mut函式目前不太穩定,但也許是時候穩定它了。

泛型原子型別

在C++中,std::atomic是泛型的:你可以有一個atomic<int>,也可以有atomic<myownstuct>。另一方面,在Rust中,我們只有特定的原子型別:AtomicU32、AtomicBool、AtomicUsize等。

C++的原子型別支援任何大小的物件,無論平臺是否支援。對於平臺本機原子操作不支援的大小的物件,它會自動返回到基於鎖的實現。Rust則只提供平臺本機支援的型別。如果你正在用沒有64位原子的平臺進行編譯,則AtomicU64不存在。

這有優點也有缺點。這意味著使用AtomicU64的Rust程式碼可能無法在某些平臺上編譯,但也意味著當某些型別默默地返回到一個非常不同的實現時,不會出現與效能相關的意外。這也意味著我們可以假設一個AtomicU64與記憶體中的u64完全相同,允許使用類似AtomicU64::from_mut的函式。在Rust中使用一個泛型原子型別atomic<T>來處理任何大小的型別可能會很棘手。沒有專門化,我們無法使automic<LargeThing>包含Mutex,而不將其包含在automic<SmallThing>中。然而,我們可以做的是將互斥量儲存在一個全域性HashMap中,由記憶體地址索引。然後,automic<T>的大小可以與T相同,並在必要時使用此全域性HashMap中的互斥量。這就是流行的atomic所做的事情。在Rust標準庫中新增這樣一個通用的範型automic<T>型別的建議需要討論它是否應該在no_std程式中使用。常規雜湊對映需要分配,這在no_std程式中是不可能的。固定大小的表可能適用於no_std程式,但由於各種原因可能不受歡迎。

Compare-exchange與填充

P0528R3更改了compare_exchange處理填充的方式。atomic上的比較交換操作也用於比較填充位,但結果證明這是一個壞主意。如今,填充位不再包括在比較中。

由於Rust目前只為整數提供原子型別,沒有任何填充,因此此更改與Rust無關。然而,使用compare_exchange方法的atomic方案需要討論如何處理填充,並且可能需要從該方案中獲取輸入。

Compare-exchange記憶體排序

在C++11中,compare_exchange函式要求成功記憶體排序至少與失敗排序一樣強。不接受compare_exchange(…,…,memory_order_release,memory_ order_ acquire)。該要求被逐字複製到Rust的compare_exchange函式中。P0418R2認為應取消此限制,這是C++17的一部分。作為Rust 1。64和Rust lang/Rust#98383的一部分,解除了相同的限制。

Constexpr互斥量建構函式

C++的std::mutex有一個constexpr建構函式,這意味著它可以在編譯時作為常量求值的一部分進行構造。然而,並非所有的實現都真正提供了這一點。例如,微軟的std::mutex實現不包括constexpr建構函式。因此,依賴這一點對於可移植程式碼來說是個壞主意。

另外,有趣的是,C++的std:: condition_variable和std:: shared_mutex根本不提供constexpr建構函式。在Rust 1。0中,Rust的原始互斥不包括常量fn new。再加上Rust對靜態初始化的嚴格要求,這使得在靜態變數中使用互斥非常煩人。這在Rust 1。63。0中作為Rust lang/Rust#93740的一部分得到了解決,所有:

Mutex:: new

rBlock:: new

Condvar:: new

現在都是常量函式。

Latches與barriers

P1135R6在C++20中引入了std::ltatch和std::barriers,這兩種型別都允許等待多個執行緒到達某一點。latch基本上只是一個計數器,它由每個執行緒遞減,並允許你等待它達到零。它只能使用一次。barrier是這種思想的更高階版本,可以重複使用,並接受計數器達到零時自動執行的“完成函式”。Rust從1。0開始就有了類似的barrier型別。它是受pthread(pthrea_Barrier_t)而不是C++的啟發。Rust的(和pthread的)barrier不如C++中現在包含的靈活。它只有一個“遞減和等待”操作(稱為等待),並且缺少C++的std::barrier附帶的“僅等待”、“僅遞減”和“遞減和刪除”函式。另一方面,與C++不同,Rust(和pthread)的“遞減和等待”操作將一個執行緒指定為組長。這是完成函式的一種(可能更靈活)替代方法。

Rust版本上缺失的操作可以在任何時候輕鬆新增。我們所需要的只是這些新方法的名稱的一個好建議。

訊號量

同樣的,P1135R6還向C++20添加了訊號量:

std::counting_semaphore

std::binary_semaphore

Rust沒有通用的訊號量型別,儘管它確實透過thread::park和unpark為每個執行緒提供了有效的二進位制訊號量。

使用Mutex<u32>和Condvar可以輕鬆地手動構建訊號量,但大多數作業系統允許使用單個AtomicU32實現更高效、更小的實現。例如,透過Linux上的futex()和Windows上的waitoAddress()。可以用於這些操作的原子大小取決於作業系統及其版本。C++的counting_semaphore是一個模板,它以一個整數作為引數來指示我們希望能夠計數到什麼程度。例如,counting_semaphore<1000>可以計數到至少1000,因此將是16位或更大。binary_semaphore型別只是counting_Sema phore<1>的別名,在某些平臺上可以是單個位元組。在Rust中,我們可能還沒有很快為這種泛型型別做好準備。Rust的泛型強制了某種一致性,這對我們可以將常量作為泛型引數進行處理帶來了一些限制。

我們可以有單獨的訊號量32、訊號量64等等,但這似乎有點過分了。擁有訊號量<u32>和訊號量<u64>甚至訊號量<bool>都是可能的,但這是我們以前在標準庫中沒有做過的事情。我們的原子型別簡單地是AtomicU32、AtomicU64等等。如上所述,對於我們的原子型別,我們只提供你正在編譯的平臺本機支援的型別。如果我們將同樣的理念應用於訊號量,它將不存在於沒有futex或WaitoAddress功能的平臺上,例如macOS。如果我們有不同大小的單獨訊號量型別,某些大小在(某些版本的)Linux和各種BSD上是不存在的。如果我們想在Rust中使用標準訊號量型別,我們首先需要一些輸入,說明我們是否確實需要不同大小的訊號量,以及需要何種形式的靈活性和可移植性才能使它們有用。也許我們應該只使用一種始終可用的32位訊號量型別(使用基於鎖的回退),但任何此類建議都必須包括對用例和限制的詳細解釋。

原子等待和通知

P1135R6新增到C++20的其餘新功能是原子等待和通知函式。

這些函式透過標準介面有效地直接公開Linux的futex()和Windows的waitoAddress()。

然而,無論作業系統支援什麼,它們都可以在所有大小的原子上、所有平臺上使用。Linux Futex(在FUTEX2之前)始終是32位的,但C++也允許atomic<uint64_t>:wait。

一種方法是使用類似於“停車場”的東西:有效地將記憶體地址對映到鎖和佇列的全域性雜湊對映。這意味著Linux上的32位等待操作可以使用非常快速的基於futex的實現,而其他大小的操作將使用非常不同的實現。如果我們遵循只提供本機支援的型別和函式的理念(就像我們對原子型別所做的那樣),我們就不會提供這樣的回退實現。這意味著我們在Linux上只有AtomicU32::wait(和AtomicI32::wait),而在Windows上,所有的原子型別都包括這個wait方法。在Rust中使用Atomic*::wait和Atomic*::notify需要討論回退到全域性表在Rust中是否合適。

jthread和stop_token

P0660R10將std::jthread和std::stop_token新增到了C ++20中。

如果我們暫時忽略stop_token,jthread基本上只是一個在銷燬時自動獲取join()方法的的常規std::thread。這避免了意外地分離執行緒並使其執行的時間比預期的長,這在常規執行緒中可能會發生。然而,它也引入了一個潛在的新陷阱:立即銷燬jthread物件將立即加入執行緒,有效地消除了任何潛在的並行性。從Rust 1。63。0開始,提供了範圍執行緒(Rust lang/Rust#93203)。與jthread一樣,作用域執行緒也會自動加入。然而,它們的連線點是明確的,並且保證安全可靠。借用檢查器甚至可以理解這一保證,允許你安全地借用作用域執行緒中的區域性變數,只要這些變數超出作用域。除了自動加入之外,jthreads的一個主要特性是其stop_token和相應的stop_ source。可以在stop_source上呼叫request_stop(),使stop_ token上相應的stopUrequest()方法返回true。這可以很好地要求執行緒停止,並在加入之前在jthread的解構函式中自動完成。由執行緒的程式碼來實際檢查令牌,並在設定時停止。到目前為止,它看起來幾乎像一個普通的AtomicBool。不同的是stop_callback型別。這種型別允許用停止令牌註冊回撥函式,即“停止函式”。使用相應的停止源請求停止將執行此功能。實際上,執行緒可以使用它來讓其他執行緒知道如何停止或取消其工作。

在Rust中,我們可以很容易地將類似atomicboolean的功能新增到thread:: Scope的Scope物件中。簡單的is_finished(&self) -> bool或stop_requested(&self) -> bool指示主作用域函式是否已完成可能就夠了。可以結合request_stop(&self)方法從任何地方請求它。

stop_callback特性更加複雜,任何Rust的等價功能都可能需要詳細的提議來討論它的介面、用例和限制。

原子浮點數

P0020R6在C++ 20中增加了對原子浮點加法和減法的支援。在Rust中新增AtomicF32或AtomicF64也很容易,但弔詭的是,似乎目前原生支援原子浮點運算的平臺往往是GPU廠商,而Rust現在好像並沒有提供對這些平臺的支援。關於向Rust新增這些型別方面,強烈建議提供一些實用的用例。

位元組原子記憶體

目前,在Rust或C++中不可能有效地實現遵循記憶體模型所有規則的序列鎖。

P1478R7建議在未來的C++版本中新增atomic_load_per_byte_memcpy和atomic_store_per_byte_memcpy來解決這個問題。

對於Rust,這裡給出一個想法,就是可以透過AtomicPerByte型別:RFC 3301來公開功能。

原子shared_ptr

P0718R2為C++20添加了atomic和atomic的專門化。

引用計數指標(C++中的shared_ptr,Rust中的Arc)通常用於併發無鎖資料結構。透過正確處理引用計數,原子<shared_ptr>專門化使正確執行此操作更加容易。

在Rust中,我們可以新增等效的AtomicArc<T>和AtomicWeak<T>型別。(雖然AtomicArc聽起來有點奇怪,但考慮到Arc的A已經代表“原子”了。)

然而,C++的shared_ptr<T>是可為空的,而在Rust中,它需要一個選項<Arc<T>。目前還不清楚AtomicArc<T>是否應該為空,或者我們是否也應該有一個AtomicOptionArc<T>。

流行的arc-swap已經在Rust中提供了所有這些變體,但據我所知,目前還沒有任何類似於標準庫的建議。

synchronized_value

儘管P0290R2沒有被接受,但提出了一種稱為synchronized_value的型別,它將互斥鎖與資料型別T組合在一起。儘管它當時沒有被C++接受,但這是一個有趣的建議,因為synchronize_value與Rust中的Mutex幾乎完全相同。

在C++中,std::mutex不包含它保護的資料,甚至根本不知道它保護的是什麼。這意味著,需要由使用者來記住哪些資料受保護以及由哪個互斥鎖保護,並確保每次訪問“受保護”資料時鎖定正確的互斥鎖。Rust的Mutex設計,使用了一個類似於(可變的)T引用的MutexGuard,這使得安全性更高,同時在只需要一個互斥鎖而不需要任何資料的情況下,仍然允許使用Mutex<()>。synchronized_value的提議試圖將此模式新增到C++中,但是使用閉包而不是互斥鎖,因為C++不跟蹤生命期。

結語

在筆者看來,C++可以繼續成為Rust的靈感來源,儘管“直接複製貼上”的想法並不值得提倡,但好的思想還是要學習和繼承的。正如我們看到的Mutex,作用域執行緒,Atomic*::from_mut等,在Rust中提供相同功能的同時,事情往往會變得非常不同。

當然,提供與C++完全相同的功能不應該是主要目標。目標應該是準確地提供Rust生態系統從語言和標準庫中需要的東西,這可能與C++使用者從他們的語言中需要的東西不同。如果你有來自Rust標準庫的併發需求,而目前還沒有滿足,歡迎把它留在評論區,不管它是否已經用另一種語言解決了。

推薦文章

  • 花生搭配它,若沒事吃點,腎可能變得越來越好

    花生搭配枸杞在日常生活中,有很多的人喜歡用枸杞泡水和英國旗,中的營養成分是非常多的,對於貧血的女性朋友來說,經常用枸杞泡水喝,可以促進人體血紅蛋白的合成,這是因為枸杞裡面含有大量的鐵元素,經常用枸杞泡水喝有利於增加人體造血原料,這樣能夠改善...

  • 劉雄輝推介湘江新區文旅:請全世界來打卡

    劉雄輝推介湘江新區文旅:請全世界來打卡上午,長沙文旅投資推介暨文旅融合發展論壇上,一批文旅專案簽約,其中包括湘江新區樂之書店全國總部專案、湘江新區當代建築文化中心專案等...

  • 有著高倉健的冷峻卻擅長演爛仔,這一次劉青雲的演技又炸了

    有著高倉健的冷峻卻擅長演爛仔,這一次劉青雲的演技又炸了在娛樂圈打拼了多年後,1994年,他在爾冬升執導的愛情電影《新不了情》中扮演懷才不遇的音樂人阿杰,愛上了袁詠儀飾演的患有骨癌的女孩阿敏,這場註定悲劇的戀愛感動了很多觀眾,劉青雲也獲得了第13屆香港電影金像獎最佳男主角提名...