对于每个 atomic 操作,都需要显示的指定 Ordering,Rust 提供了 Relaxed,Release,Acquire,AcqRel,以及 SeqCst 这些 Ordering 的支持,使用不同的 Ordering 会让编译器或者 CPU 对某些指令进行重新排序执行,所以为了更正确的写出 lock-free 的代码,了解这些 Ordering 是如何工作的,就显得非常重要了。

rust 提供的 Ordering 支持有: Relaxed,Release,Acquire,AcqRel,以及 SeqCst。

关键字

描述atomic 操作之间关系的概念,synchronizes-with 和 happens-before。

synchronizes-with

简单来说,两个线程 A 和 B,以及一个支持原子操作的变量 x,如果 A 线程对 x 写入了一个新的值(store),而 B 线程在 x 上面读取到了这个新的值(load),我们就可以认为,A 的 store 就是 synchronizes-with B 的 load 的。

操作 A synchoronizes-with 操作 B(A->B)

Happens-before

如果一个操作 B 能看到之前操作 A 产生的结果,那么 A 就是 happens-before B 的。譬如在单线程里面,如果一个操作 A 的语句在操作 B 的前面执行,通常叫做 sequenced-before,那么 A 就是 happens-before B 的。

几种 Order 的介绍

Relaxed ordering

适用场景:需要对某个变量进行原子操作,而不需要考虑多个线程同步的情况,譬如,reference counter。

Acquire-Release ordering

Acquire 和 Release 通常都是需要成对使用的。
当对 store 使用 Release ordering 之后,后续任何的 Acquire ordering 的 load 操作,都会看到之前 store 的值。
通过 Acquire-Release,我们能支持 synchronizes-with。

  1. fn write_x_then_y() {
  2. X.store(true, Ordering::Relaxed);
  3. Y.store(true, Ordering::Release);
  4. }
  5. fn read_y_then_x() {
  6. while !Y.load(Ordering::Acquire) {}
  7. if X.load(Ordering::Relaxed) {
  8. Z.fetch_add(1, Ordering::SeqCst);
  9. }
  10. }
  11. fn main() {
  12. let t1 = thread::spawn(move || {
  13. write_x_then_y();
  14. });
  15. let t2 = thread::spawn(move || {
  16. read_y_then_x();
  17. });
  18. t1.join().unwrap();
  19. t2.join().unwrap();
  20. assert_ne!(Z.load(Ordering::SeqCst), 0);
  21. }

Sequence ordering

不光提供了 Acquire 和 Release 的 ordering 支持,同时也确保所有的线程看到完全一致的原子操作顺序。

  1. fn write_x() {
  2. X.store(true, Ordering::SeqCst); // 1
  3. }
  4. fn write_y() {
  5. Y.store(true, Ordering::SeqCst); // 2
  6. }
  7. fn read_x_then_y() {
  8. while !X.load(Ordering::SeqCst) {}
  9. if Y.load(Ordering::SeqCst) { // 3
  10. Z.fetch_add(1, Ordering::SeqCst);
  11. }
  12. }
  13. fn read_y_then_x() {
  14. while !Y.load(Ordering::SeqCst) {}
  15. if X.load(Ordering::SeqCst) { // 4
  16. Z.fetch_add(1, Ordering::SeqCst);
  17. }
  18. }
  19. fn main() {
  20. let t1 = thread::spawn(move || {
  21. write_x();
  22. });
  23. let t2 = thread::spawn(move || {
  24. write_y();
  25. });
  26. let t3 = thread::spawn(move || {
  27. read_x_then_y();
  28. });
  29. let t4 = thread::spawn(move || {
  30. read_y_then_x();
  31. });
  32. t1.join().unwrap();
  33. t2.join().unwrap();
  34. t3.join().unwrap();
  35. t4.join().unwrap();
  36. assert_ne!(Z.load(Ordering::SeqCst), 0);
  37. }

上面的例子中,只有使用 SeqCst ordering,才能保证 Z 最后的值不为 0,任何其他的 ordering,都不能保证。
Write X,Y 发生在不同的线程中,当 t3 线程先读到 X,后读到 Y,那么 t4 线程也肯定会先读到 X,后读到 Y。
SeqCst 在有些时候会有性能瓶颈。因为它要确保操作在所有线程之前全局同步。

Memory fence

还可以使用 memory fence 来保证 synchronizes-with,如下:

  1. fn write_x_then_y() {
  2. X.store(true, Ordering::Relaxed); // 1
  3. fence(Ordering::Release); // 2
  4. Y.store(true, Ordering::Relaxed); // 3
  5. }
  6. fn read_y_then_x() {
  7. while !Y.load(Ordering::Relaxed) {} // 4
  8. fence(Ordering::Acquire); // 5
  9. if X.load(Ordering::Relaxed) { // 6
  10. Z.fetch_add(1, Ordering::SeqCst);
  11. }
  12. }
  13. fn main() {
  14. let t1 = thread::spawn(move || {
  15. write_x_then_y();
  16. });
  17. let t2 = thread::spawn(move || {
  18. read_y_then_x();
  19. });
  20. t1.join().unwrap();
  21. t2.join().unwrap();
  22. assert_ne!(Z.load(Ordering::SeqCst), 0);
  23. }

2 Release fence 是 synchronizes-with 5 Acquire fence,所以 1 肯定会在 6 之前执行。

Epilogue

要弄清楚 memory ordering,其实并不是一件容易的事情,不过多数时候,为了不出错,使用 SeqCst 就成。

Go memory model

Dekker’s test - Litmus test

Litmus test is a tool to enumerate all the possibilities of CPU execution.
http://diy.inria.fr/www/?recore=aarch64#

Go 夜读笔记
不同的编译器以及微处理器结果不一样。
编译器行为包括:

  • Constant folding(常量提前计算)
  • Code motion(不影响函数的情况下会对一些执行顺序做移动)

注:

  1. No clear defintion for atomics so far;
  2. Some operaions will set up Happens-Before relationsship;
  3. Extended discussion: Dose explicit synchronization always be slower than atomic? Yes, in most cases. But…(以后的 hardware transcation 的方式如果没有资源竞争时,性能会更好)

For Gophers

  • Do not communicate by sharing memory; instead, share memory by communicating.
  • Go race dector may help to exam some memory coherency issues.(Note: There might be false positives and false negatives!)
  • Don’t be clever!