print!和println!宏实现
在上一章中我们实现了基本的打印功能,现在的打印功能使用起来不是很方便,因此我们对之前编写打印功能进行优化
自旋锁
原子操作
为了更好理解自旋锁,我们需要了解一下原子操作,原子操作指在执行过程中不会被任何其它任务或事件中断,一个任务要么做要么不做,不能在做的过程中被打断,这个特性需要硬件支持在x86平台上,CPU提供了在指令执行期间对总线加锁的手段
Rust中的原子操作
在Rust中我们可以使用std::sync::atomic
包来使用原子操作,在#![no_std]
环境中我们可以使用core::sync::atomic
使用
atomic包提供了AtomicBool,AtomicIsize,AtomicUsize,AtomicI8,AtomicU16等类型的原子操作
每个方法都会使用Ordering
枚举表明内存屏障的强度,Rust的原子顺序LLVM原子顺序一致,原子类型可以存储在静态变量中,可以使用常量初始化程序(如AtomicBool :: new)进行初始化
原子访问可以告诉硬件和编译器,我们的程序是多线程的。每一个原子访问都关联一种 “排序方式”,以确定它和其他访问之间的关系。归根结底,就是告诉编译器和硬件什么是它们不能做的。对于编译器,主要指的是命令的重排。而对于硬件,指的是写操作的结果如何同步到其他的线程
— 《Rust高级编程》
Ordering
内存顺序(Memory orderings)指定原子操作同步内存的方式,以下是常用的几种状态
- Relaxed
- Release(获取)
- Acquire(释放)
- AcqRel
- SeqCst(顺序一致性)
以下内容摘自《Rust高级编程》,感觉解释比我好,PS:才不是因为懒 (笑)
顺序一致性
顺序一致性是所有排序方式中最强大的,包含了其他所有排序方式的约束条件。直观上看,顺序一致性操作不能被重排:在同一个线程中,SeqCst 之前的访问永远在它之前,之后的访问永远在它之后。只使用顺序一致性原子操作和数据访问就可以构建一个无数据竞争的程序,这种程序的好处是它的命令在所有线程上都有着唯一的执行流程。而且这个执行流程又很容易推导:它就是每个线程各自执行流程的交叉。如果你使用更弱的原子排序方式的话,这一点并不一定继续有效。
顺序一致性给开发者的便利并不是免费的。即使是在强顺序平台上,顺序一致性也会产生内存屏障 (memory fence)。
事实上,顺序一致性很少是程序正确性的必要条件。但是,如果你对其他内存排序方式模棱两可的话,顺序一致性绝对是你正确的选择。程序执行得稍微慢一点总比执行出错要好!将它变为具有更弱一致性的原子操作也很容易,只要把
SeqCst
变成Relaxed
就完工了!当然,证明这种变化的正确性就是另外一个问题了。
获取 - 释放
获取和释放经常成对出现。它们的名字就提示了它们的应用场景:它们适用于获取和释放锁,确保临界区不会重叠。
直观看起来,acquire 保证在它之后的访问永远在它之后。可在它之前的操作却有可能被重排到它后面、类似的,release 保证它之前的操作永远在它之前。但是它后面的操作可能被重排到它前面。
当线程 A 释放了一块内存空间,紧接着线程 B 获取了同一块内存,这时因果关系就确定了。在 A 释放之前的所有写操作的结果,B 在获取之后都能看到。但是,它们和其他线程之间没有确定因果关系。同理,如果 A 和 B 访问的是不同的内存,它们也没有因果关系。
所以,释放 - 获取的基本用法很简单:你获取一块内存并进入临界区,然后释放内存并离开临界区
use std::sync::Arc; use std::sync::atomic::{AtomicBool, Ordering}; use std::thread; fn main() { let lock = Arc::new(AtomicBool::new(false)); // 我上锁了吗? // ...用某种方式把锁分发到各个线程... // 设置值为true,以尝试获取锁 while lock.compare_and_swap(false, true, Ordering::Acquire) {} // 跳出循环,表明我们获取到了锁! // ...恐怖的数据访问... // 工作完成了,释放锁 lock.store(false, Ordering::Release); }
在强顺序平台上,大多数的访问都有释放和获取的语义,释放和获取通常是无开销的。不过在弱顺序平台上不是这样。
Relaxed
Relaxed 访问是最弱的。它们可以被随意重排,也没有先后关系。但是 Relaxed 操作依然是原子的。也就是说,它并不算是数据访问,所有对它的读 - 修改 - 写操作都是原子的。Relaxed 操作适用于那些你希望发生但又并不特别在意的事情。比如,多线程可以使用 Relaxed 的 fetch_add 来增加计数器,如果你不使用计数器的值去同步其他的访问,这个操作就是安全的。
在强顺序平台上使用 Relaxed 没什么好处,因为它们通常都有释放 - 获取语义。不过,在弱顺序平台上,Relaxed 可以获取更小的开销。
自旋锁的实现
根据之前的理论,我们现在就开始实现自旋锁
我们创建一个新的项目称为system,我们编写的代码最终要以库的形式使用,因此在创建的时候选择创建库(library)而不是可执行(executable)
通过一下命令便可以创建
cargo new system --lib
我们的项目结构为
<