Murat Aslan 2024-05-03
原子操作:不可分割的构建基石 —— 并发与并行
想象一个微小而自包含的世界,其中某个动作瞬间完成,没有任何中断。这正是 Rust 中原子操作的本质。这些底层类型支持无锁并发(lock-free concurrency),意味着它们可以在不依赖笨重锁机制的情况下对共享内存位置进行操作。这带来了更流畅的性能表现,尤其是在多个线程同时访问相同数据的场景中。
原子操作是 Rust 中更复杂并发抽象的基本构建块。它们保证整个操作——无论是存储值(store)、读取值(load)、交换值(swap),还是在获取的同时加法(fetch_add)——都作为一个单一、不可分割的单元执行。这可以防止竞态条件(race conditions)——即多个线程同时尝试访问或修改同一数据,从而可能导致意外结果的棘手情况。
Rust 中常见的原子操作:
- store:以原子方式将一个值写入内存位置,确保整个写入操作在其他线程能看到更改之前完成。
- load:以原子方式从内存位置读取一个值,确保线程接收到的是最新写入的值,而不是由于并发访问导致的半写入状态。
- swap:以原子方式用新值替换现有值,并返回旧值。这对于实现无锁数据结构(如基于 compare-and-swap (CAS) 的链表)非常有用。
- fetch_add:以原子方式将一个值加到内存位置中的现有值上,并返回求和结果。该操作特别适用于创建并发计数器。
- compare_and_swap (CAS):这一强大操作将现有值与提供的值进行比较。如果两者匹配,则以原子方式将其替换为新值。这是构建线程安全数据结构的基石。
内存屏障:在并发世界中确保顺序
虽然原子操作保证了操作本身的不可分割性,但它们并不规定周围指令的执行顺序。这就是内存屏障(Memory Barriers)发挥作用的地方。可以把它们想象成控制内存操作对其他线程可见顺序的同步点。
内存屏障在原子操作与普通内存访问之间强制实施特定的顺序。这确保了线程始终看到一致的数据状态,避免了诸如陈旧读取(stale reads)或意外行为等问题。Fence 是一种特定类型的内存屏障,它作为强同步点,防止某些类型的内存操作在其周围被重排序。
协同之力:原子操作与内存屏障的优势
通过结合原子操作和内存屏障的优势,Rust 程序员可以获得以下好处:
- 提升性能:无锁并发通常比传统的锁机制带来更好的性能,尤其在高竞争(high contention)场景下。
- 增强线程安全性:原子操作和内存屏障提供了围绕数据访问的保证,防止竞态条件,并确保跨线程的数据一致性。
- 构建复杂抽象的基础:这些底层操作构成了更高层并发抽象(如互斥锁 mutexes 和通道 channels)的基础,使程序员能够构建健壮的并发系统。
原子操作和内存屏障是进入 Rust 并发编程精彩世界所必需的工具。通过理解其原理并有效利用它们,你可以编写出安全、高效且可扩展的并发应用程序,充分发挥多线程的真正威力。请记住,这些操作构成了更复杂抽象的基石,为在 Rust 中构建健壮且高性能的并发系统奠定了基础。
使用原子操作实现并发计数器(Rust 代码)
假设有一个网站,访客可以点击“点赞”按钮来表达对某篇文章的喜爱。为了追踪总点赞数,我们需要一个并发计数器,能够被多个用户同时安全地递增。以下是我们在 Rust 中使用原子操作实现这一功能的方式:
use std::sync::atomic::{AtomicUsize, Ordering};
// 定义一个结构体来保存点赞计数器
struct LikeCounter {
likes: AtomicUsize,
}
impl LikeCounter {
fn new() -> Self {
LikeCounter { likes: AtomicUsize::new(0) }
}
// 以原子方式递增点赞数的函数
fn increment(&self) {
self.likes.fetch_add(1, Ordering::Relaxed);
}
// 获取当前点赞数的函数(非原子)
fn get_likes(&self) -> usize {
self.likes.load(Ordering::Relaxed)
}
}
fn main() {
let counter = LikeCounter::new();
// 模拟多个用户并发地递增计数器
for _ in 0..100 {
let thread_counter = counter.clone();
std::thread::spawn(move || {
thread_counter.increment();
});
}
// 等待所有线程完成
std::thread::sleep(std::time::Duration::from_millis(100));
// 获取最终的点赞数(由于是非原子读取,可能不准确)
let final_likes = counter.get_likes();
println!("总点赞数: {}", final_likes);
}
说明:
- 我们使用
AtomicUsize以原子方式存储点赞计数。 LikeCounter结构体封装了计数器,并提供了递增和读取计数的函数。increment使用fetch_add(1, Ordering::Relaxed)以原子方式将 1 加到计数器上,并返回之前的值。Ordering::Relaxed表示对内存顺序的要求最低。get_likes使用load(Ordering::Relaxed)读取当前的点赞数。这不是原子操作,意味着获取的值可能无法反映最新的递增结果,因为编译器可能会对指令进行重排序。- 在
main函数中,我们创建了一个LikeCounter实例,并生成多个线程来模拟并发递增。 - 我们等待所有线程完成,以确保所有递增操作都已执行完毕。
- 最后,我们调用
get_likes(非原子)来获取最终计数,但由于是非原子读取,该值可能并不完全准确。
实际应用中的考量:
- 虽然此处为了简化使用了
Ordering::Relaxed,但在具体需求中你可能需要更严格的内存顺序(memory ordering)。 - 在真实应用中,你可能希望更频繁地显示点赞数。可以使用一个单独的原子变量来表示显示用的计数(更新频率较低),这样可以在保持整体一致性的同时提升性能。
原子操作的优势:
本示例展示了像 fetch_add 这样的原子操作如何在并发环境中确保点赞计数器的安全和一致更新。这避免了竞态条件,并保证了总点赞数的数据完整性。