Rust 的所有权系统天然排斥共享可变状态 —— 同一时刻只能有一个可变借用,或多个不可变借用。这一规则在编译期消除了数据竞争,却让需要跨线程共享可变数据的场景成为工程难题。本文聚焦三条实现路径的工程权衡:Arc+Mutex 的同步原语方案内部可变性模式(RefCell/Cell/ UnsafeCell),以及unsafe 块的极限优化。作者 Alice Ryhl 在其关于共享可变状态的经典文章中系统阐述了这一主题,本文的参数建议也源于其实践总结。

标准方案:Arc+Mutex 的安全基石

当需要跨线程共享可变数据时,标准答案是 Arc<Mutex<T>>Arc<RwLock<T>>Arc 提供共享所有权的引用计数能力,Mutex(互斥锁)则确保同一时刻只有一个线程可以访问内部数据。Alice Ryhl 推荐的最小化封装结构如下:

use std::sync::{Arc, Mutex};

#[derive(Clone)]
pub struct SharedState {
    inner: Arc<Mutex<SharedStateInner>>,
}

struct SharedStateInner {
    counter: u64,
    data: Vec<String>,
}

impl SharedState {
    pub fn new() -> Self {
        Self {
            inner: Arc::new(Mutex::new(SharedStateInner {
                counter: 0,
                data: Vec::new(),
            })),
        }
    }

    pub fn increment(&self) {
        let mut lock = self.inner.lock().unwrap();
        lock.counter += 1;
    }

    pub fn get_counter(&self) -> u64 {
        let lock = self.inner.lock().unwrap();
        lock.counter
    }
}

这种模式将锁的获取细节封装在类型内部,调用方无需知道同步机制的存在。关键工程参数:锁粒度应尽可能细,持有时间不超过 100 微秒(异步场景)或 1 毫秒(同步场景),以避免严重性能退化。

在异步代码中使用标准库的 Mutex 存在一个致命陷阱:禁止在持有锁时执行 .await。编译器在多数情况下会捕获这一问题并报出 "future cannot be sent between threads safely" 错误,因为 MutexGuard 未实现 Send。最佳实践是始终在非 async 函数中完成锁定操作,然后在异步上下文中调用这些同步方法:

impl Debouncer {
    pub fn reset_deadline(&self) {  // 同步方法,内部锁定
        let mut lock = self.inner.lock().unwrap();
        lock.deadline = Instant::now() + lock.duration;
    }

    pub async fn sleep(&self) {  // 只读取,不锁定
        loop {
            let deadline = self.get_deadline();  // 辅助函数获取值
            if deadline <= Instant::now() {
                return;
            }
            tokio::time::sleep_until(deadline).await;
        }
    }

    fn get_deadline(&self) -> Instant {
        let lock = self.inner.lock().unwrap();
        lock.deadline
    }
}

如果确实需要在持锁时 await,应使用 tokio::sync::Mutex(异步锁)。但 Alice Ryhl 强调,异步锁比阻塞锁慢 3-5 倍,且无法在析构函数中使用,应作为最后手段。

内部可变性:RefCell 的单线程妥协

对于单线程场景下的共享可变需求,RefCell<T> 提供了运行时可变的内部可变性。它通过 borrow()borrow_mut() 方法在运行时检查借用规则,违反时 panic。典型用法是配合 Rc 实现引用计数:

use std::cell::RefCell;
use std::rc::Rc;

let data = Rc::new(RefCell::new(vec![1, 2, 3]));
let data_clone = Rc::clone(&data);

data_clone.borrow_mut().push(4);  // 运行时借用检查
println!("{:?}", data.borrow());  // [1, 2, 3, 4]

工程阈值:当借用检查频率超过每秒 10 万次时,RefCell 的运行时检查开销可能成为瓶颈,此时应考虑裸指针配合 unsafe(但需极度谨慎)。

Cell<T> 提供更轻量的内部可变性,适用于 Copy 类型,通过 get()set() 直接取值 / 赋值,无运行时借用检查。对于高性能场景,如果数据结构足够简单,CellRefCell2-3 倍

对于线程安全需求,std::sync::RwLock 是读取密集型场景的首选 —— 多个读取器可并发访问,只有一个写入者。Alice Ryhl 特别指出,使用 RwLock 时需警惕饥饿问题:大量读取者可能导致写入者长时间无法获取锁。parking_lot 库的 RwLock 采用公平策略,可缓解此问题。

unsafe 块:性能极限与安全边界

当标准同步原语成为性能瓶颈时,unsafe 块提供了绕过借用检查的能力。UnsafeCell<T> 是 Rust 中唯一的合法可变引用_cell 容器,所有内部可变性机制(RefCellMutexRwLock)底层都依赖它。

use std::cell::UnsafeCell;

unsafe {
    let ptr = data.get();
    // 手动保证:同一时刻只有一个可变借用
    (*ptr).push(42);
}

使用 unsafe 意味着程序员承担了编译器原本负责的安全保证。适用场景极其有限:实现高性能无锁数据结构、FFI 交互、需要零开销抽象的底层库。工程决策清单

  • 共享状态修改频率是否超过 每秒 50 万次?否则 Mutex 足够。
  • 是否需要无锁语义(lock-free)?是 → 使用 AtomicU64/AtomicPtr 或相关 crate。
  • 是否必须消除所有同步开销?确认分析表明同步原语是瓶颈后再考虑 unsafe。

常见的 unsafe 误用场景包括:企图用 unsafe 绕过借用检查实现 "全局可变状态",这实际上回到了 C 语言的数据竞争陷阱。Alice Ryhl 在讨论中明确反对这种做法 —— 即使在 unsafe 块中,也必须手动保证内存安全。

替代方案:按场景选型

场景 推荐方案 关键参数
跨线程共享 map Arc<RwLock<HashMap>>dashmap 碎片数 = CPU 核心数 × 2
读多写少 Arc<RwLock<T>>arc-swap 写操作频率 < 1%
高频计数器 Arc<AtomicU64> 无锁,原子操作
线程局部缓存 thread_local! 每线程独立副本
最终一致性 map evmap 写入延迟容忍 < 100ms
异步持锁等待 tokio::sync::Mutex 仅在必要时使用

dashmap 是一个值得关注的库,它将 map 拆分为多个碎片,每个碎片独立加锁,可实现接近无锁的并发吞吐量。但其 MutexGuard 实现了 Send,编译器不会在 await 时捕获死锁,使用时必须严格遵守 "仅在同步函数中锁定" 的规范。

工程决策框架

共享可变状态的实现选择应遵循以下决策树:首先评估是否跨线程 —— 单线程场景用 Rc<RefCell<T>>,多线程场景进入下一步。其次评估同步原语是否足够 —— 大多数场景下 Arc<Mutex<T>>Arc<RwLock<T>> 已足够高效,只有在 ** profiling 明确显示锁竞争是瓶颈 ** 时才考虑更复杂的方案。最后评估是否需要无锁或极限性能 —— 此时方可使用 UnsafeCell 配合手动同步,但必须进行 haustive 的并发测试。

Alice Ryhl 的核心建议是:将锁封装在类型内部,而非暴露给调用方。这不仅简化了 API,更重要的是将同步策略的变更(Mutex 改为 RwLock,或改为无锁结构)的影响限制在单一模块内。

对于异步代码,永远不要在持有锁时 await,这应作为铁律。编译器无法捕获所有此类错误(使用 spawn_local 时),而死锁的调试成本极高。


资料来源:本文技术细节主要参考 Alice Ryhl 在个人博客发表的《Shared mutable state in Rust》一文,该文系统阐述了 Rust 中共享可变状态的最佳实践。