1. 背景
在Rust開發過程中,很多情況下需要在不可變的情況下獲取可變性或者在多線程的情況下可以安全的貢獻可變數據。這種情況下我們一般使用**Mutex
來實現通過加鎖來實現。現在我們可以通過使用SyncUnsafeCell
來替代Mutex
**。
2. SyncUnsafeCell
SyncUnsafeCell
是 Rust 標準庫中的一個類型,用于在多線程環境中安全地共享可變數據。它是 UnsafeCell
的一個包裝,提供了額外的同步機制。
作用
- 共享可變數據:在 Rust 中,默認情況下,數據是不可變的,且不能在多個線程之間共享可變數據。
SyncUnsafeCell
允許你在多線程環境中共享可變數據。 - 內部可變性:
SyncUnsafeCell
提供了內部可變性,這意味著你可以在不獲取可變引用的情況下修改其內容。這對于需要在多線程環境中共享可變狀態的場景非常有用。 - 安全性:雖然名字中包含 “Unsafe”,但
SyncUnsafeCell
在多線程環境中提供了一定程度的安全性。它通過內部的同步機制確保了對UnsafeCell
的訪問是安全的。
使用場景
- 多線程共享狀態:當你需要在多個線程之間共享可變狀態時,可以使用
SyncUnsafeCell
。 - 性能優化:在某些情況下,使用
SyncUnsafeCell
可以比使用Mutex
或RwLock
等同步原語更高效,因為它提供了更細粒度的控制。
3. SyncUnsafeCell在Rocketmq-rust中的應用
在**TopicConfigManager
** 功能模塊宗,因為很多方法都是使用了不可變引用 &self
那么需要修改**data_version
** 就必須使用可變引用。為了解決這個問題就使用**Mutex
** 來實現。如下圖:
但是這種情況下每次獲取可以變引用都需要進行加鎖才能獲取。而這里的同步性是可預見的。不存在數據競爭所以使用**SyncUnsafeCell
來替換Mutex
** 減少加鎖帶來的性能消耗。
這里為什么不使用**UnsafeCell
** 因為在rocketmq-rust項目中需要Sync也就是:
4.SyncUnsafeCell和Mutex的bench表現測試
我們使用下面的代碼進行測試(測試代碼參照:https://github.com/mxsm/rocketmq-rust/blob/main/rocketmq-broker/benches/syncunsafecell_mut.rs):
#![feature(sync_unsafe_cell)]
use std::cell::SyncUnsafeCell;
use std::collections::HashSet;use criterion::criterion_group;
use criterion::criterion_main;
use criterion::Criterion;pub struct Test {pub a: SyncUnsafeCell<HashSet<String>>,pub b: parking_lot::Mutex<HashSet<String>>,
}impl Test {pub fn new() -> Self {Test {a: SyncUnsafeCell::new(HashSet::new()),b: parking_lot::Mutex::new(HashSet::new()),}}pub fn insert_1(&self, key: String) {unsafe {let a = &mut *self.a.get();a.insert(key);}}pub fn insert_2(&self, key: String) {let mut b = self.b.lock();b.insert(key);}pub fn get_1(&self, key: &str) -> String {unsafe {let a = &*self.a.get();a.get(key).unwrap().to_string()}}pub fn get_2(&self, key: &str) -> String {let b = self.b.lock();b.get(key).unwrap().as_str().to_string()}
}fn benchmark_insert_1(c: &mut Criterion) {let test = Test::new();c.bench_function("insert_1", |b| {b.iter(|| {test.insert_1("key".to_string());})});
}fn benchmark_insert_2(c: &mut Criterion) {let test = Test::new();c.bench_function("insert_2", |b| {b.iter(|| {test.insert_2("key".to_string());})});
}fn benchmark_get_1(c: &mut Criterion) {let test = Test::new();let key = String::from("test_key");// Insert key for the get benchmarkstest.insert_1(key.clone());c.bench_function("get_1", |b| {b.iter(|| {test.get_1("test_key");})});
}fn benchmark_get_2(c: &mut Criterion) {let test = Test::new();let key = String::from("test_key");// Insert key for the get benchmarkstest.insert_2(key.clone());c.bench_function("get_2", |b| {b.iter(|| {test.get_2("test_key");})});
}criterion_group!(benches,benchmark_insert_1,benchmark_insert_2,benchmark_get_1,benchmark_get_2
);
criterion_main!(benches);
測試命令:
cargo bench
執行結果:
要比較 insert_1
和 insert_2
方法的優劣,我們需要考慮以下幾個方面:
-
執行時間:
insert_1
: 平均執行時間約為 42.637 ns。insert_2
: 平均執行時間約為 54.484 ns。
從執行時間上看,
insert_1
明顯比insert_2
快。 -
性能變化:
insert_1
: 性能提升了約 5.8%。insert_2
: 性能下降了約 2.5%。
insert_1
的性能提升,而insert_2
的性能下降。 -
異常值數量:
insert_1
: 4個異常值,3個輕微異常,1個嚴重異常。insert_2
: 4個異常值,均為輕微異常。
雖然兩者的異常值數量相同,但
insert_1
的異常值有一個嚴重異常,而insert_2
的異常值均為輕微異常。
綜合來看,insert_1
方法在執行時間和性能提升方面明顯優于 insert_2
,但需要注意的是 insert_1
存在一個嚴重異常的情況。這表明在大多數情況下 insert_1
是更好的選擇,但在某些極端情況下可能會有性能波動。
4.1 get_1 和 get_2 方法的比較
-
執行時間:
get_1
: 平均執行時間約為 49.228 ns。get_2
: 平均執行時間約為 50.564 ns。
從執行時間上看,
get_1
稍微比get_2
快。 -
異常值數量:
get_1
: 7個異常值,1個輕微異常,6個嚴重異常。get_2
: 4個異常值,均為輕微異常。
get_1
存在較多的嚴重異常值,而get_2
異常值較少且均為輕微異常。
結論
- 插入操作:
insert_1
是更好的選擇,因為它的執行時間更短且性能提升顯著。雖然存在一些嚴重異常,但整體表現優于insert_2
。 - 獲取操作:
get_1
的平均執行時間比get_2
快,但存在較多嚴重異常。如果系統對性能一致性要求較高,get_2
可能是更好的選擇,因為它的異常值較少且均為輕微異常。
總的來說,如果追求平均性能且可以接受一定程度的性能波動,insert_1
和 get_1
是較好的選擇;如果追求性能的一致性,insert_2
和 get_2
可能更適合。
讓我們分析 insert_1
基準測試的結果:
4.2 圖表分析
左側圖表(密度圖)
- X軸:代表插入操作的平均時間(以納秒為單位)。
- Y軸:代表密度(密度單位)。
- 藍色區域:顯示了每次迭代所花費時間的概率分布,密度圖的高峰顯示了最可能的時間范圍。
- 藍色垂直線:代表平均時間。對于
insert_1
操作,平均時間大約為 42.637 ns。
右側圖表(總樣本時間)
- X軸:表示迭代次數(以百萬次為單位)。
- Y軸:表示總樣本時間(以毫秒為單位)。
- 藍色點:顯示了總樣本時間的線性回歸,表明每次迭代的時間是相對恒定的。
附加統計數據
- Slope:斜率,估算的每次迭代所需時間。
- 下限:42.590 ns
- 估算值:42.637 ns
- 上限:42.686 ns
- R2:決定系數,表示數據擬合度。值越接近1,擬合度越高。
- R2:0.9954266
- Mean:平均時間。
- 42.661 ns
- Std. Dev.:標準差,表示數據的離散程度。
- 391.47 ps
- Median:中位數,表示數據的中間值。
- 42.591 ns
- MAD:中位絕對偏差,表示數據的波動性。
- 219.56 ps
結論
- 平均時間:
insert_1
操作的平均時間為 42.637 ns。這表明操作的時間開銷非常小。 - 一致性:高 R2 值(0.9954266)表明基準測試結果非常一致,操作時間非常穩定。
- 波動性:標準差(391.47 ps)和中位絕對偏差(219.56 ps)都非常小,表明操作時間波動很小。
總的來說,insert_1
操作性能非常穩定且高效,時間開銷非常小。