并发原语与无锁编程

Rust 并发底层实践:内存模型与 Ordering(Relaxed/Acquire/Release/AcqRel/SeqCst)语义、Mutex 与 RwLock 选型(std 与 parking_lot 对比)、Atomic 基础与 CAS 循环、无锁 SPSC 环形队列实现、ABA 与伪共享陷阱、何时该放弃无锁。

Rust 用 Send/Sync 在编译期挡掉了数据竞争,但挡不住逻辑竞态:两个原子操作之间的窗口、错误的内存序、被优化掉的可见性,编译器都不会替你检查。Arc<Mutex<T>> 能解决 90% 的场景,剩下 10% 需要原子类型和无锁结构。

本文先讲清楚 Ordering 到底在约束什么,再对比锁的选型,最后给出一份可运行的无锁队列骨架和几个真实踩坑点。前提是已经理解线程与 Send/Sync,基础可参考 Rust 异步与并发编程 。

内存模型与 Ordering

Rust 的内存模型沿用 C++11:多核 CPU 和编译器都可以重排内存访问,Ordering 是给它们的约束。五个取值:

Ordering约束典型用途
Relaxed只保证原子性,不保证顺序计数器、统计量
Acquire读之后的操作不能上移到它前面加锁、读取「已发布」数据
Release写之前的操作不能下移到它后面解锁、发布数据
AcqRel同时具备 Acquire + Releasefetch_add 类读改写
SeqCst全序,所有线程看到同一顺序难以推理时保底

happens-before 才是关键

Relaxed 单独用几乎总是错的,除非只关心数值不关心关联数据。经典的「发布-订阅」模式:

use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::Arc;

static READY: AtomicBool = AtomicBool::new(false);
static DATA: AtomicUsize = AtomicUsize::new(0);

// 线程 A:先写数据,再置标志
DATA.store(42, Ordering::Relaxed);
READY.store(true, Ordering::Release);      // Release:DATA 的写不会跑到它后面

// 线程 B:先看标志,再读数据
while !READY.load(Ordering::Acquire) {}    // Acquire:DATA 的读不会跑到它前面
assert_eq!(DATA.load(Ordering::Relaxed), 42);

如果两边都用 Relaxed,B 可能看到 READY == true 却读到 DATA == 0。Release/Acquire 配对建立了 happens-before 边,才保证「看到标志即看到数据」。

经验法则:用最弱的、你能证明正确的 Ordering。证明不了就用 SeqCst——性能损失通常远小于调试一个只在 ARM 上复现的竞态。

各架构差异

x86 是强内存模型,Relaxed 与 SeqCst 的差别很小;ARM/PowerPC 是弱模型,错误的内存序会在那里暴露。在 x86 上测试通过不代表正确,CI 里跑 ARM 或加 --cfg 压力测试很有价值。

Mutex 与 RwLock 选型

std 与 parking_lot

特性std::sync::Mutexparking_lot::Mutex
大小较大(含系统 futex 状态)1 字节
锁中毒有(PoisonError)无
公平性平台相关默认较公平,可配置
性能无竞争时更快有竞争时更快,自旋更积极
调试标准库集成支持 deadlock_detection feature
use parking_lot::Mutex;   // 无 poison,unwrap() 可以直接省掉

let m = Mutex::new(0);
{
    let mut g = m.lock();      // 直接返回 guard,不是 Result
    *g += 1;
}

RwLock 什么时候值得

RwLock 读多写少才划算,因为它的开销比 Mutex 高:

use std::sync::RwLock;

let cache = RwLock::new(HashMap::new());
{
    let r = cache.read().unwrap();       // 多读者可并发
    r.get(&key)
}
{
    let mut w = cache.write().unwrap();  // 写者独占
    w.insert(key, value);
}

判断依据是读写比与临界区长度:读写比低于约 10:1,或临界区只有几个指令,Mutex 反而更快。std::sync::RwLock 还可能出现写者饥饿(读持续到来导致写永远拿不到锁),对写延迟敏感的服务应改用 parking_lot::RwLock 并开启公平模式。

减少临界区

比选锁更重要的是缩小临界区:

// ❌ 在锁内做 I/O 或重计算
let mut map = state.lock().unwrap();
let v = slow_compute(&key);      // 锁被长期持有
map.insert(key, v);

// ✅ 先算好再进锁
let v = slow_compute(&key);
state.lock().unwrap().insert(key, v);

分片锁(sharded lock)是另一个常见手段:把 HashMap 拆成 16 个分片,各配一把锁,冲突概率降到 1/16。

Atomic 基础

标准库提供的原子类型覆盖整数、布尔和指针:AtomicUsize、AtomicI64、AtomicBool、AtomicPtr<T> 等。复合类型(如 AtomicU64 在 32 位平台上)可能由锁模拟,需查 cfg(target_has_atomic)。

常用操作

use std::sync::atomic::{AtomicUsize, AtomicU64, Ordering};

let counter = AtomicUsize::new(0);
counter.fetch_add(1, Ordering::Relaxed);        // 返回旧值
counter.fetch_sub(1, Ordering::AcqRel);
counter.load(Ordering::Relaxed);
counter.store(10, Ordering::Release);
counter.swap(5, Ordering::AcqRel);              // 交换并返回旧值

CAS 循环

compare_exchange 是构建无锁结构的基石,返回 Result<旧值, 当前值>:

use std::sync::atomic::{AtomicUsize, Ordering};

fn increment_if_below(max: usize, counter: &AtomicUsize) -> bool {
    let mut cur = counter.load(Ordering::Relaxed);
    loop {
        if cur >= max {
            return false;
        }
        match counter.compare_exchange_weak(
            cur, cur + 1,
            Ordering::AcqRel,       // 成功时的序
            Ordering::Relaxed,      // 失败时的序
        ) {
            Ok(_) => return true,
            Err(actual) => cur = actual,   // 被别人改过,用新值重试
        }
    }
}

循环里用 compare_exchange_weak(允许伪失败,在 LL/SC 架构上生成更好的代码),只有在循环外只用一次时才用 _strong。

无锁队列:SPSC 环形缓冲

单生产者单消费者(SPSC)是无锁结构里最容易写对的,核心是 head/tail 两个原子索引:

use std::cell::UnsafeCell;
use std::sync::atomic::{AtomicUsize, Ordering};

pub struct SpscRing<T, const N: usize> {
    buf: [UnsafeCell<Option<T>>; N],
    head: AtomicUsize,   // 消费者位置
    tail: AtomicUsize,   // 生产者位置
}

unsafe impl<T: Send, const N: usize> Sync for SpscRing<T, N> {}

impl<T, const N: usize> SpscRing<T, N> {
    pub fn push(&self, item: T) -> Result<(), T> {
        let tail = self.tail.load(Ordering::Relaxed);
        let head = self.head.load(Ordering::Acquire);   // 看到消费者的释放
        if tail.wrapping_sub(head) == N {
            return Err(item);                            // 满
        }
        let idx = tail % N;
        unsafe { *self.buf[idx].get() = Some(item); }
        // Release:保证写入对消费者可见
        self.tail.store(tail.wrapping_add(1), Ordering::Release);
        Ok(())
    }

    pub fn pop(&self) -> Option<T> {
        let head = self.head.load(Ordering::Relaxed);
        let tail = self.tail.load(Ordering::Acquire);    // 看到生产者的发布
        if head == tail {
            return None;                                 // 空
        }
        let idx = head % N;
        let item = unsafe { (*self.buf[idx].get()).take() };
        self.head.store(head.wrapping_add(1), Ordering::Release);
        item
    }
}

要点:索引用 wrapping_add 而非取模递增,避免溢出;生产者的 Release 与消费者的 Acquire 配对,保证数据先于索引可见;UnsafeCell 是「我能改但编译器别假设」的唯一合法容器。

生产环境优先用 crossbeam 或 rtrb 等成熟 crate。自己写的无锁结构必须过 Loom 的模型检验,否则等于没测。

陷阱与验证

ABA 问题

CAS 只比较值,不比较「是否被改过又改回来」。指针栈的 pop 中,若 A 被弹出、释放、又分配回同一地址,CAS 会误判成功。解法是加版本号(AtomicU64 打包 指针+版本),或用 crossbeam_epoch 做内存回收。

伪共享(False Sharing)

两个无关的原子变量落在同一条缓存行(通常 64 字节),一个核改它会失效另一个核的缓存行:

use crossbeam_utils::CachePadded;

struct Counters {
    a: CachePadded<AtomicUsize>,   // 各自独占缓存行
    b: CachePadded<AtomicUsize>,
}

高频更新的原子计数器一定要 CachePadded,否则多核扩展性会随核数下降。

内存序用错的隐蔽性

错误的内存序在 x86 上往往「看起来对」,在 ARM 上偶发失败。用 Loom 做穷举式交错验证:

#[test]
fn spsc_model() {
    loom::model(|| {
        let q = loom::sync::Arc::new(loom::sync::Mutex::new(0));
        // loom 会枚举所有线程交错,找出违反不变量的一处
    });
}

loom 把 std::sync::atomic 换成自己的实现,穷举所有可能的执行顺序,能抓出人工测试永远复现不了的竞态。无锁代码提交前跑一遍 loom 应成为硬性要求。

何时不该无锁

信号建议
临界区包含多个变量、需要整体一致用 Mutex,无锁很难维护不变量
竞争激烈(写多)无锁的 CAS 重试风暴比锁更慢
团队没有无锁经验用 crossbeam/dashmap,别自研
需要阻塞等待、条件变量Condvar + Mutex 才是对的工具
只是偶尔读写的配置RwLock 或 arc-swap

无锁的价值在于消除不可控的阻塞和优先级反转,而不是「一定更快」。测出来才算数,别凭直觉选。Rust 的并发安全边界(Send/Sync、unsafe 的正确使用)与 所有权与内存安全 一脉相承,而底层原子指令与缓存行为的硬件视角可延伸阅读 Rust 系统编程 。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「rust」更多文章

  1. 性能剖析与优化
  2. 测试与基准:criterion 与 proptest
  3. Serde 与序列化生态