sui_rust/ch02/
s4_lockfree.rs

1//! 第二章:Rust核心概念
2//! 2.3 Lockfree
3//!
4
5/**
6
7    ### 并发编程注重的三点:
8
9    1. 原子性。保证操作是原子的。
10    2. 可见性。保证数据是同步的。
11    3. 顺序性。保证操作的顺序是正确的。
12
13    方法:
14
15    - 同步锁
16    - 无锁编程
17
18
19    ### 思考:锁带来的问题?
20
21    1. 性能。引入无锁编程可以最大化减少线程上下文切换,线程等待。
22    2. 死锁。引入无锁编程就不会产生死锁。
23
24    无锁编程性能并不是总是优于锁同步。
25
26    无锁编程依赖于原子类型,使用原子类型还需要深入了解一些概念。
27
28
29    ###  理解无锁并发的关键在于理解计算机组成
30
31    ```text
32    +------+    +------+    +------+
33    | core |    | core |    | core |
34    +---+--+    +---+--+    +---+--+
35        |           |           |
36    +lv1+--+    +-lv1--+    +lv1---+
37    |cache |    |cache |    | cache|
38    +---+--+    +---+--+    +---+--+
39        |           |           |
40    +lv2---+    +lv2---+    +lv2---+
41    | cache|    | cache|    | cache|
42    +---+--+    +---+--+    +---+--+
43        |           |           |
44        +-----------------------+
45                    |
46    +---------------+--------------+
47    |           lv3 cache          |
48    +---------------+--------------+
49                    |
50                    |
51    +---------------+--------------+
52    |          main memory         |
53    +------------------------------+
54
55    ```
56
57    ### 缓存一致性问题
58
59    思考:下面代码最终执行后 x 和 y 的状态上什么?
60
61    ``` text
62    // THREAD 1
63
64    if unsafe { *x == 1 } {
65        unsafe { *y += 1 }
66    }
67
68    // THREAD 2
69    unsafe {
70        *y = 10;
71        *x = 1;
72    }
73    ```
74
75    最终结果实际取决于下面几个问题:
76
77    1. THREAD 1 和 THREAD 2 运行顺序是什么? (使用锁同步可以保证“喂给” CPU 的指令锁有顺序性的)
78    2. 每一个 CPU 使用的高速缓存的状态 (要保证缓存一致性)
79    3. CPU 指令重排(乱序执行,为了更好的利用流水线,达到性能极致)
80    4. 编译器指令重排 (调整指令顺序,使其对 CPU 更友好)
81
82
83    > 高速缓存通过 M(Modified)E(Exclusive)S(Shared)I(Invalid) 协议来保持同步。MESI的有趣之处在于,每个高速缓存行都在维护自己的内存地址状态机。
84    > 一个 CPU 通过 IPC (Inter-Processor-Communication) 向 另一个 CPU 来发送消息,比如,「独占内存地址」、「修改内存数据」等。
85    > 这里要注意,CPU 并不是做来某些操作马上发出消息或者是收到消息马上就执行相应操作的。也就是说,CPU 通信的这些消息都是异步的。
86    > 如果 CPU 多核之间用同步通信的话,性能上无法接受。比如一个 CPU 要等待其他 CPU 来确认信息,或从其他 CPU 获取最新数据等。
87    > 所以,为了让 CPU 核之间可以高性能地同步信息(保证cpu乱序执行指令的同时,还要保证程序正确性),就引入了内存屏障的技术。
88
89
90    ### 内存屏障
91
92    内存屏障允许开发者在编写代码等时候在需要等地方加入它。
93
94    内存屏障分为:
95
96    1. 读屏障(Load Memory Barrier)。任何「读屏障」前的「读操作」都会先于「读屏障」后的「读操作」完成。
97    2. 写屏障(Store Memory Barrier)。任何「写屏障」前的「写操作」都会先于「写屏障」后的「写操作」完成。
98    3. 全屏障。同时包含读屏障和写屏障的作用。
99
100    load,代表从内存读数据。store,代表向内存写数据。
101
102    现代 CPU 架构中一般分为四种内存屏障:
103
104    1. Load-Load: 等价于上面介绍的「读屏障」。任何「该屏障」前的「读操作」都会先于「该屏障」后的「读操作」完成。
105    2. Load-Store: 任何「该屏障」前的「读操作」都会先于「该屏障后」的「写操作」完成。
106    3. Store-Load: 任何「该屏障」前的「写操作」都会先于「该屏障后」的「读操作」完成。
107    4. Store-Store: 等价于上面介绍的「写屏障」。任何「该屏障」前的「写操作」都会先于「该屏障」后的「写操作」完成。
108
109
110    开发者通过内存屏障,告诉 CPU/编译器 内存屏障前后指令的顺序关系,这样 CPU/编译器 就不会重排这些指令,从而保证原子指令的顺序性。
111
112    ### 内存模型
113
114    这么多内存屏障,什么时候该使用哪种,需要由开发者来指定。这道理和 Unsafe Rust 类似,编译器无法检查的安全性,交给开发者。
115
116    这就是语言提供内存模型的原因。Cpp 和 Rust 语言都提供了原子(Atomic)类型,并且这些原子类型都是可以指定内存顺序(告诉CPU使用哪种内存屏障)
117
118    Rust 目前并没有正式的内存顺序模型,但是它在语义和行为上和 Cpp 一致。
119
120    由此引申出内存屏障都两种语义:
121
122    1. acquire(获取)语义。Load 之后的读写操作无法被重排至 Load 之前。即 相当于Load-Load和Load-Store屏障。
123    2. release(释放)语义。Store 之前的读写操作无法被重排至 Store 之后。即 相当于Load-Store和Store-Store屏障。
124
125
126    ### 原子操作
127
128    Rust 标准库中定义的原子类型:[std::sync::atomic: https://doc.rust-lang.org/stable/std/sync/atomic/index.html](https://doc.rust-lang.org/stable/std/sync/atomic/index.html)
129    其中`// std::sync::atomic::Ordering`定义了 Rust 支持的内存顺序,官方文档指出,当前和 Cpp20 的内存顺序是一致的。
130
131
132    ```rust
133    // std::sync::atomic::Ordering
134
135    pub enum Ordering {
136        Relaxed,
137        Release,
138        Acquire,
139        AcqRel,
140        SeqCst,
141    }
142    ```
143
144    注意,Rust 当前并没有公开 cpp 里面包含对 consume 语义。
145
146    内存顺序说明:
147
148    - Relaxed,表示原子类型只保证原子操作即可,没有内存顺序(不指定内存屏障)
149    - Release,
150        - 表示使用 Release 语义。
151        - 当前线程内的所有写操作,对于其他对这个原子变量进行 acquire 的线程可见
152    - Acquire,
153        - 表示使用 Acquire 语义。
154        - acquire 可以保证读到所有在 release 前发生的写入。
155    - AcqRel,
156        - 表示对读取和写入施加 acquire-release 语义,无法被重排。
157        - 可以看见其他线程施加 release 语义的所有写入,同时自己的 release 结束后所有写入对其他施加 acquire 语义的线程可见。
158    - SeqCst,
159        - 如果是读取就是 acquire 语义,如果是写入就是 release 语义,如果是读取+写入就是 acquire-release 语义。
160        - 所有线程都能以相同的顺序看到所有顺序一致的操作。
161
162    不同对内存顺序,对应不同对内存屏障,进一步,也代表了不同的性能。
163    在竞争条件比较激烈的情况下,Relaxed 性能是最好的,因为它不需要任何内存屏障,这就意味着CPU之间不需要进行一致性同步。
164    相对而言,SeqCst 就是性能最差的那个了,因为它需要 CPU 同步所有指令。
165    但是 Relaxed 因为没有内存屏障,所以可能会有指令重排带来带风险。
166    所以,Rust 标准库也提供了` std::sync::atomic::compiler_fence`和` std::sync::atomic::fence`两个函数,来帮助解决在原子指令使用 Relaxed 内存顺序的情况下编译器或CPU指令重排的问题。
167
168    示例:[https://doc.rust-lang.org/stable/std/sync/atomic/fn.compiler_fence.html](https://doc.rust-lang.org/stable/std/sync/atomic/fn.compiler_fence.html)
169
170    原子类型提供的方法,基于硬件原子指令 (x均为std::atomic)	:
171
172    - `load`,返回x的值。
173    - `store`,把x设为n,什么都不返回。
174    - `swap`,把x设为n,返回设定之前的值。
175    - `compare_and_swap`,经典cas操作。
176    - `compare_exchange.
177    - `compare_exchange_weak
178    - `fetch_add(n), fetch_sub(n)`,原子地做x += n, x-= n,返回修改之前的值。
179
180
181    使用原子类型需要注意的是:
182
183    - Store操作,可选内存顺序:Relaxed, Release, SeqCst。否则panic。
184    - Load操作,可选内存顺序:Relaxed, Acquire, SeqCst。否则panic。
185    - Read-modify-write(读-改-写)操作,可选如下顺序:Relaxed, Acquire, Release, AcqRel, SeqCst。
186    - 所有操作的默认顺序都是 SeqCst。
187
188     ```rust
189    // 实现一个简单的自旋锁(spinlock)
190
191    use std::sync::Arc;
192    use std::sync::atomic::{AtomicUsize, Ordering};
193    use std::{thread, time};
194
195    fn main() {
196        let spinlock = Arc::new(AtomicUsize::new(1));
197
198        let spinlock_clone = Arc::clone(&spinlock);
199        let thread = thread::spawn(move|| {
200            // lock
201            spinlock_clone.store(1, Ordering::SeqCst);
202            // do something
203            let t = time::Duration::from_secs(3);
204            std::thread::sleep(t);
205            // unlock
206            spinlock_clone.store(0, Ordering::SeqCst);
207        });
208
209        // Wait for the other thread to release the lock
210        while spinlock.load(Ordering::SeqCst) != 0 {}
211
212        if let Err(panic) = thread.join() {
213            println!("Thread had an error: {:?}", panic);
214        }
215    }
216
217    ```
218
219    利用 AtomicBool 实现一个 轻量级的锁 :
220
221    ```rust
222    use std::sync::atomic::Ordering;
223    use core::sync::atomic::AtomicBool;
224
225    struct LightLock(AtomicBool);
226
227    impl LightLock {
228        pub fn new() -> LightLock {
229            LightLock(AtomicBool::new(false))
230        }
231
232        pub fn try_lock<'a>(&'a self) -> Option<LightGuard<'a>> {
233            let was_locked = self.0.swap(true, Ordering::Acquire);
234            if was_locked {
235                None
236            } else {
237                Some(LightGuard { lock: self })
238            }
239        }
240    }
241
242    struct LightGuard<'a> {
243        lock: &'a LightLock,
244    }
245
246    impl<'a> Drop for LightGuard<'a> {
247        fn drop(&mut self) {
248            self.lock.0.store(false, Ordering::Release);
249        }
250    }
251    ```
252
253    ### ABA 问题:
254
255    任何无 GC 的语言在无锁编程的时候都要考虑此问题。
256
257    试想一个连续压栈(push)和 出栈(pop)的并发操作。假设这两个操作都是由 cas 原语实现的。
258
259    > 具体来说,假如有两个线程 T1 和 T2,操作初始栈为「a->b->c」的栈结构。
260    > 当 T1 把 a 从栈内弹出,此时发生线程调度,
261    > 切换到 T2 ,T2 弹出 a 和 b,把 a 再 push 到栈里,此时 T2 的栈为 「a->c」。
262    > 然后线程切回 T1 ,T1 看到栈顶(a)的地址和它之前获得的 a 地址相同,然后将 栈顶设置为 b (a.next),然而 b 早就被释放来。
263
264    这就是 ABA 问题。ABA 问题本质是内存回收问题。当 b 被弹出当时候,要保障它当内存不能被立即重用。
265
266    解决该问题的思路有多种:引用计数、分代回收(Epoch Based Reclamation)和 险象指针(Hazard pointer)。
267
268    注意:ABA 问题一般是发生在 X86 架构上 cas 原子操作的时候。ARM 架构已经从根源上解决了 ABA 问题。
269
270    - 分代回收示例:[https://github.com/crossbeam-rs/crossbeam/blob/master/crossbeam-epoch/examples/sanitize.rs](https://github.com/crossbeam-rs/crossbeam/blob/master/crossbeam-epoch/examples/sanitize.rs)
271    - 险象指针示例:
272        - [https://github.com/solotzg/rs-lockfree/blob/master/src/hazard_pointer.rs](https://github.com/solotzg/rs-lockfree/blob/master/src/hazard_pointer.rs)
273        -  [https://github.com/redox-os/conc/blob/master/src/sync/treiber.rs](https://github.com/redox-os/conc/blob/master/src/sync/treiber.rs)
274
275
276
277*/
278pub fn memory_reordering() {}