金属Rust:原子操作
对于复杂的操作来说,使用互斥量(Mutex)来避免竞态条件相当省力。但是对于一些比较小规模的操作,比如让一个计数器+1之类,会考虑更方便的原子对象。
原子类型在标准库中的std::sync::atomic
模块下。原子类型和平时使用的基础类型(primitive type)很像,唯一的区别是原子类型的操作能够保证对数据操作的访问顺序。也就是说,如果当前线程对一个变量进行的修改如果没有完成,其他线程是无法访问到该变量的。
操作顺序
对于一些比较耗时的操作,CPU会采用乱序执行的策略来保证执行效率。但是这也带来了一个问题:代码的执行顺序被改变了,如果这个变量。而这个顺序变化是由CPU产生的,关编译器优化也救不了你。那么如果不同的线程在同时访问两个不同的原子变量,执行逻辑可能就会发生改变:
比如说你觉得它是这么执行的:
- 线程A向flag1写入
true
; - 线程B读取flag1,发现值为
true
,继续执行; - 线程A向flag2写入
true
; - 线程B读取flag2,发现值为
true
,继续执行; - 执行业务逻辑。
然而实际上,它是这么执行的:
- 线程A向flag2写入
true
; - 线程B读取flag1,发现值为
false
,放弃执行; - 线程A向flag1写入
true
。
整个逻辑树都错误了!于是为了处理这种情况,我们会使用std::sync::atomic::Ordering
来确定执行顺序。保证期望操作会按期望种的顺序执行。
Rust的执行顺序基本和LLVM的对应:
顺序 | 说明 |
---|---|
Relaxed |
弃疗,只进行原子操作不管执行顺序。简单的计数器可以考虑使用这个。 |
Acquire |
如果是读取,保证在这次原子操作后的代码在其之后执行,之前的操作可能会被置后。阻止处理器执行后续指令。 |
Release |
如果是写入,保证在这次原子操作前的代码在其之前执行,后续的操作可能会被提前。让处理器执行完所有前面的指令。 |
AcqRel |
在读取的时候等同于Acquire ;在写入的时候等同于Release 。但这并不代表AcqRel 会根据load() /store() 自动适配,实际上load() /store() 的时候使用AcqRel 会导致线程panic。 |
SeqCst |
保证指令位置和处理器执行位置 完 全 一 致。保证完全的顺序正确,是最安全的顺序,但是可能会降低执行速度。 |
举个例子
但是这个执行顺序到底是用来干嘛的?原子操作跟操作重排有什么关系?
我们拿std::sync::atomic::AtomicBool
来举个例子。比如说有个原子布尔量叫做flag
,我们通过这个原子量来实现一个自旋锁。flag
的值为true
的时候表示线程得锁。
while flag.compare_and_swap(false, true, Ordering::Acquire) {
yield_now()
}
// 访问临界数据..
flag.store(false, Ordering::Release);
使用Acquire
顺序保证了,在夺得锁之前,所有应该在夺锁后发生的操作不会被排到前面去。比如说后面要操作一个RefCell
,莫名其妙在拿到锁之前就进行borrow_mut()
,而这个时候如果夺到锁的线程还在可变借用这个RefCell
,线程就会因为访问问题panic了。
而后面的Release
顺序保证了,在释放锁的时候,所有应该在释放前进行的操作都已经完成。也可以参考刚才Acquire
的例子,在释放锁之后才调用了borrow_mut()
什么的。
你可能已经发现了,Acquire
和Release
顺序的名称其实是指对原子锁的操作:“acquire a lock”和“release a lock”。不过毕竟我阅历浅薄,对于原子操作有严格顺序要求的需求暂时还只见过这个例子,无法进行更多的叙述了。
AcqRel
的情况比较特殊,可能后面会另外开篇文章说一下。
请发表评论