在併發編程領域,Rust 提供了完善的機制來保證併發編程的安全,我們可以非常方便的使用 Mutex,Arc,Channel 等庫來處理我們的併發邏輯。 但在有些時候,爲了更高效的性能,我們可能會去寫一些 lock-free 的數據結構,而 Rust 自身也提供了 atomic 的支持。
對於每個 atomic 操作,都需要顯示的指定 Ordering,Rust 提供了 Relaxed,Release,Acquire,AcqRel,以及 SeqCst 這些 Ordering 的支持,使用不同的 Ordering 會讓編譯器或者 CPU 對某些指令進行重新排序執行,所以爲了更正確的寫出 lock-free 的代碼,瞭解這些 Ordering 是如何工作的,就顯得非常重要了。
Keywords
在開始介紹 Rust 的 memory ordering 之前,我們需要知道兩個常用的用來描述atomic 操作之間關係的概念,synchronizes-with 和 happens-before:
Synchronizes-with - 簡單來說,兩個線程 A 和 B,以及一個支持原子操作的變量 x,如果 A 線程對 x 寫入了一個新的值(store),而 B 線程在 x 上面讀取到了這個新的值(load),我們就可以認爲,A 的 store 就是 synchronizes-with B 的 load 的。
Happens-before - 這應該算是一個更基礎的概念。如果一個操作 B 能看到之前操作 A 產生的結果,那麼 A 就是 happens-before B 的。譬如在單線程裏面,如果一個操作 A 的語句在操作 B 的前面執行,通常叫做 sequenced-before,那麼 A 就是 happens-before B 的,譬如這樣:
vec.push(1); // A
ready = true; // B
而對於跨線程(inter-thread) 的情況,要判斷 happens-before,就需要藉助於前面提到的 synchronizes-with 了。如果操作 A 是 synchronizes-with 另一個線程的操作 B 的,那麼 A 就是 happens-before B 的。Happens-before 也具有傳遞性,如果 B 是 happens-before C 的,那麼 A 也是 happens-before C。
如果 A 是 sequenced-before B,而 B 是 inter-thread happens-before C 的,那麼 A 也是 inter-thread happens-before C。同理,如果 A inter-thread happens-before B,而 B sequenced-before C,那麼 A 也是 inter-thread happens-before C。
可以看到,要寫出正確的 atomic 代碼,尤其是在多線程環境下面,關鍵就是要弄清楚兩個 atomic 操作的 syncrhonizes-with 關係。而這個不同的 Ordering 是不一樣的。
Relaxed ordering
Relaxed ordering 只能保證原子操作,在同一個線程裏面,對同一個變量的操作會滿足 happens-before 關係,但對於 inter-thread 來說,它不能提供 synchronizes-with 支持,並不保證任何順序。
下面是一個簡單的例子:
fn write_x_then_y() {
X.store(true, Ordering::Relaxed);
Y.store(true, Ordering::Relaxed);
}
fn read_y_then_x() {
while !Y.load(Ordering::Relaxed) {}
if X.load(Ordering::Relaxed) {
Z.fetch_add(1, Ordering::SeqCst);
}
}
fn main() {
let t1 = thread::spawn(move || {
write_x_then_y();
});
let t2 = thread::spawn(move || {
read_y_then_x();
});
t1.join().unwrap();
t2.join().unwrap();
assert_ne!(Z.load(Ordering::SeqCst), 0);
}
上面 assert 可能會失敗,也就是 Z 的值在最後可能爲 1。在函數 write_x_then_y
裏面即使 store X happens-before store Y,即使在 read_y_then_x
裏面 load Y 返回了 true,X 的值仍然可能是 false。因爲對 X 和 Y 的兩個操作都是 relaxed 的,雖然對於不同的線程,兩個 load 或者兩個 store 都可能滿足 happens-before,但在 store 和 load 之間,並沒有相關的約束,也就是意味着 load 可能看到亂序的 store。
通常來說,relaxed 適用的場景就是需要對某個變量進行原子操作,而且不需要考慮多個線程同步的情況,譬如 reference counter,其它時候需要考慮有更強約束的其他 ordering。
Acquire-Release ordering
Acquire 和 Release 通常都是需要成對使用的,當對 store 使用 Release ordering 之後,後續任何的 Acquire ordering 的 load 操作,都會看到之前 store 的值。也就是說,通過 Acquire-Release,我們能支持 synchronizes-with。
fn write_x_then_y() {
X.store(true, Ordering::Relaxed);
Y.store(true, Ordering::Release);
}
fn read_y_then_x() {
while !Y.load(Ordering::Acquire) {}
if X.load(Ordering::Relaxed) {
Z.fetch_add(1, Ordering::SeqCst);
}
}
fn main() {
let t1 = thread::spawn(move || {
write_x_then_y();
});
let t2 = thread::spawn(move || {
read_y_then_x();
});
t1.join().unwrap();
t2.join().unwrap();
assert_ne!(Z.load(Ordering::SeqCst), 0);
}
不同於之前的 relaxed,這裏我們對 Y 使用了 Acquire 和 Release,那麼最後 Z 就一定不可能爲 0 了。主要是因爲 store Y 是 synchronizes-with load Y 的,也就是 store Y happens before load Y,因爲 store X 是 sequenced-before store Y,那麼 store X 就是 happens-before load X 的。
通常,我們還可以使用 AcqRel ordering,它其實就是組合了 Acquire 和 Release,對於 load 使用 Acquire,而對於 store 則是使用 Release。
Sequence ordering
Sequence ordering 不光提供了 Acquire,Release 的 ordering 支持,同時也確保所有線程會看到完全一致的原子操作順序。
fn write_x() {
X.store(true, Ordering::SeqCst); // 1
}
fn write_y() {
Y.store(true, Ordering::SeqCst); // 2
}
fn read_x_then_y() {
while !X.load(Ordering::SeqCst) {}
if Y.load(Ordering::SeqCst) { // 3
Z.fetch_add(1, Ordering::SeqCst);
}
}
fn read_y_then_x() {
while !Y.load(Ordering::SeqCst) {}
if X.load(Ordering::SeqCst) { // 4
Z.fetch_add(1, Ordering::SeqCst);
}
}
fn main() {
let t1 = thread::spawn(move || {
write_x();
});
let t2 = thread::spawn(move || {
write_y();
});
let t3 = thread::spawn(move || {
read_x_then_y();
});
let t4 = thread::spawn(move || {
read_y_then_x();
});
t1.join().unwrap();
t2.join().unwrap();
t3.join().unwrap();
t4.join().unwrap();
assert_ne!(Z.load(Ordering::SeqCst), 0);
}
上面的例子,只有使用 SeqCst ordering,才能保證 Z 最後的值不爲 0,任何其他的 ordering,都不能保證,我們來具體分析一下。因爲兩個 read 函數都是有 while 循環,退出之前一定能確保 write 函數被調用了。因爲使用 SeqCst 能保證所有線程看到一致的操作順序,假設 3 返回了 false,表明 X 爲 true,而 Y 爲 false,這時候一定能保證 store Y 還沒調用,一定能保證 store X 在 store Y 之前發生,4 就一定會返回 true。
如果這裏我們對 load 使用 Acquire,而對 store 使用 Release,read_x_then_y
和 read_y_then_x
可能看到完全相反的對 X 和 Y 的操作順序。
SeqCst 在有些時候,可能會有性能瓶頸,因爲它需要確保操作在所有線程之前全局同步,但是它其實又是最直觀的一種使用方式, 所以通常,當我們不知道用什麼 ordering 的時候,用 SeqCst 就對了。
Memory fence
出了使用不同的 ordering,我們還可以使用 memory fence 來支持 synchronizes-with,如下:
fn write_x_then_y() {
X.store(true, Ordering::Relaxed); // 1
fence(Ordering::Release); // 2
Y.store(true, Ordering::Relaxed); // 3
}
fn read_y_then_x() {
while !Y.load(Ordering::Relaxed) {} // 4
fence(Ordering::Acquire); // 5
if X.load(Ordering::Relaxed) { // 6
Z.fetch_add(1, Ordering::SeqCst);
}
}
fn main() {
let t1 = thread::spawn(move || {
write_x_then_y();
});
let t2 = thread::spawn(move || {
read_y_then_x();
});
t1.join().unwrap();
t2.join().unwrap();
assert_ne!(Z.load(Ordering::SeqCst), 0);
}
在上面的例子中,2 Release fence 是 synchronizes-with 5 Acquire fence 的,而4 load Y 的時候一定會讀取到 3 store Y 的值,加上 1 store X 是 sequenced-before 3 的,那麼自然能確定 1 是 happens-before 6 的。也就是 Z 一定不會等於 0。
Epilogue
可以看到,要弄清楚 memory ordering,其實並不是一件容易的事情,不過多數時候,爲了不出錯,使用 SeqCst 就成。