Rust 中的 Atomic 类型

Atomic 类型用于包装一个整数,保证对这个整数的操作是原子的,并且可以指定内存顺序。用于多线程场景。

如果多个线程同时持有一个普通整数,并且至少有一个线程执行写操作,那么就会有 data race 即 UB 的风险。举几个例子:

  • 两个线程都进行 x += 1,这是一个典型的读改写,由于整个操作非原子,两次修改可能基于同一个原始值,最终 x 的值并没有增加 2.
  • 一个线程等待 x != 0 发生再退出循环,另一个线程修改 x。由于编译器不知道 x 是一个多线程访问的变量,可能将 x != 0 优化为 tmp = x, tmp != 0。此时另一个线程修改 x 不会影响 tmp 的值,导致死循环。
  • 两个线程同时执行 x = 1 和 print(x),由于没有同步要求,编译器可能从硬件指令上对这些操作做各种优化,导致最终 print(x) 打印的值并非原始值或 1,出现了 UB。

也就是说,即使我们不要求读改写原子,只要求简单的赋值操作完整、跨线程可见,我们也需要使用 Atomic。否则编译器可能在"无需同步"的假设下,做出各种优化,导致 UB。

Atomic 类型提供一系列 API,如 fetch_add 等。在编译这些 API 时,编译器不会随意优化,可能还会使用特殊的硬件指令保证写入结果同步到各个 CPU 上。(这里可能涉及缓存一致性协议)从而避免了上面提到的可能出现的 UB。

如果在实际编程时不好想象使用简单的整数类型会引发什么问题,可以选择 坚定的相信 在"多个线程同时持有一个普通整数,并且至少有一个线程执行写操作" 的场景下,一定要使用 Atomic。

Ordering

fetch_add 等 API 中有一个参数叫 Ordering,他是用于限制访存操作的内存顺序的。因为默认情况下,CPU 和 编译器 可能进行指令重排序的优化,即代码中编写的变量赋值顺序(单个线程中)可能与实际发生的顺序不同。

举一个例子

rust 复制代码
// 线程 1
data = 42;
ready.store(true, Release);

// 线程 2
if ready.load(Acquire) {
    println!("{}", data);
}

通过使用 Release / Acquire 保证:store(true) 发生时,前面的 data 一定被赋值为了 42;load 发生后,线程 2 才会去获取 data 的值。如果这两个有一个没有得到保证,就可能发生 ready 是 true 了,但 data 不是 42 的情况。

所以 Release 和 Acquire 的语义是这样的

  • Release:适用于写操作,要求此次写操作之前的内存访问一定都完成了,再执行写操作。
  • Acquire:适用于读操作,要求此次读操作完成后,再进行后续的内存访问操作。

Release / Acquire 必须成对、对同一个变量使用,才能达成在线程间同步的效果。

其他的 Ordering 还有 Relaxed、AcqRel、SeqCst 等,就不一一赘述了。

相关推荐
传奇开心果编程12 分钟前
【Xilem 0.4 基础语法学与练】第二课视图修饰符、列表渲染、组件拆分、memoize 性能优化
学习·rust·前端框架
孙启超2 小时前
【AI开发之Rust】第 1 课:认识 Rust 与开发环境 —— 装好工具链,跑通第一个程序
开发语言·人工智能·后端·ai·rust·安全架构
老猿讲编程3 小时前
【Eclipse OpenSOVD学习之十四】差距清单与演进建议
学习·rust·eclipse·车载·sovd
传奇开心果编程4 小时前
【Xilem 0.4 基础语法学与练】第17课:Lens 机制——状态切片与子组件复用
学习·rust·前端框架
老猿讲编程6 小时前
【Eclipse OpenSOVD学习之十一】网关二进制与部署
学习·中间件·rust·eclipse·汽车
传奇开心果编程15 小时前
【Xilem 0.4 基础语法学与练】第一课:从零到计数器
学习·rust·前端框架
Flynt1 天前
pnpm 12 换上了 Rust 内核,我拿项目实测了一轮构建速度
rust·vite·前端工程化
传奇开心果编程1 天前
【Xilem基础语法学与练】第8课:条件渲染(one_of)
学习·rust·前端框架
qwsaedca1 天前
在Mac上跑 Kokoro TTS经验总结
rust·mac·tts