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 等,就不一一赘述了。

相关推荐
SomeB1oody4 小时前
【RustyML入门】1.0. 快速上手
开发语言·后端·机器学习·rust·教程
骊城英雄4 小时前
Rust从入门到精通-trait
人工智能·算法·rust
花褪残红青杏小6 小时前
Rust图像处理第23节-图像加噪:给每个像素单独掷一次骰子
rust·webassembly·图形学
小杍随笔11 小时前
2025年Rust GUI框架实战万字避坑指南
开发语言·后端·rust
在水一缸1 天前
当 AI 编码助手遇上 Rust:深入解析 Zerostack 的极简哲学与实战应用
rust·系统架构·开源项目·轻量化·ai编码助手·zerostack
2401_873479402 天前
风控引擎高并发IP查询方案对比:开源Rust库 vs 商业离线库,怎么选?
tcp/ip·rust·开源·ip
花褪残红青杏小2 天前
Rust图像处理第22节-从RGB到YCbCr: 让亮度和颜色分家
rust·webassembly·图形学
花褪残红青杏小2 天前
Rust图像处理第21节-最小二乘回归:用矩阵求逆解"拟合"问题
rust·webassembly·图形学
k4m7v2pz2 天前
Bevy 0.14.2 玩家精灵不渲染(只有背景在动)排查全记录
macos·rust·bevy
老王生涯2 天前
rust开发环境配置-Windows & GNU
开发语言·windows·rust