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

相关推荐
G_dou_17 小时前
Linux 搭建 Rust 开发环境:从 rustup 安装到 Cargo 镜像
linux·rust
小灰灰搞电子20 小时前
Rust 实现异步ModbusTCP主机源码分享
服务器·网络·modbustcp·rust
小杍随笔21 小时前
【Rust 工具链管理完全指南:rustup toolchain 命令实战详解】
开发语言·后端·rust
Vallelonga1 天前
Rust 中 unsafe 关键字的语义
开发语言·rust
小杍随笔1 天前
【Rust 1.96.0 深度解析:让 Range 可 Copy、让断言更聪明、让 Wasm 更安全】
安全·rust·wasm
lpfasd1231 天前
Mise 安装与配置避坑全攻略
rust
星秀日2 天前
rust学习入门
开发语言·学习·rust