penguinliong

金属Rust:原子操作

对于复杂的操作来说,使用互斥量(Mutex)来避免竞态条件相当省力。但是对于一些比较小规模的操作,比如让一个计数器+1之类,会考虑更方便的原子对象。

原子类型在标准库中的std::sync::atomic模块下。原子类型和平时使用的基础类型(primitive type)很像,唯一的区别是原子类型的操作能够保证对数据操作的访问顺序。也就是说,如果当前线程对一个变量进行的修改如果没有完成,其他线程是无法访问到该变量的。

操作顺序

对于一些比较耗时的操作,CPU会采用乱序执行的策略来保证执行效率。但是这也带来了一个问题:代码的执行顺序被改变了,如果这个变量。而这个顺序变化是由CPU产生的,关编译器优化也救不了你。那么如果不同的线程在同时访问两个不同的原子变量,执行逻辑可能就会发生改变:

比如说你觉得它是这么执行的:

  1. 线程A向flag1写入true
  2. 线程B读取flag1,发现值为true,继续执行;
  3. 线程A向flag2写入true
  4. 线程B读取flag2,发现值为true,继续执行;
  5. 执行业务逻辑。

然而实际上,它是这么执行的:

  1. 线程A向flag2写入true
  2. 线程B读取flag1,发现值为false,放弃执行;
  3. 线程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()什么的。

你可能已经发现了,AcquireRelease顺序的名称其实是指对原子锁的操作:“acquire a lock”和“release a lock”。不过毕竟我阅历浅薄,对于原子操作有严格顺序要求的需求暂时还只见过这个例子,无法进行更多的叙述了。

AcqRel的情况比较特殊,可能后面会另外开篇文章说一下。

分类:

技术点:

相关文章:

猜你喜欢