You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Rust中不安全定义可跨线程共享并修改的结构体?

在Rust中手动实现自定义同步的跨线程可变结构体

问题说明

需要不安全地实现一个可在多线程间共享并修改的结构体,不依赖Mutex或RwLock,自行控制线程安全逻辑;出于性能考虑,在确定处于非临界区时要跳过锁检查。仅使用Arc会遇到可变借用错误:cannot borrow data in an Arc as mutable,原因是Arc未实现DerefMut trait。

安全实现示例(基于Mutex)

struct Foo {
    bar: usize,
}

impl Foo {
    pub fn set_bar(&mut self, a: usize) {
        self.bar = a;
    }
}

fn main() {
    let mut foo = Foo { bar: 32 };
    foo.bar = 33;

    let foo_arc = std::sync::Arc::new(std::sync::Mutex::new(foo));
    let foo_arc_2 = std::sync::Arc::clone(&foo_arc);

    let handle = std::thread::spawn(move || {
        foo_arc_2.lock().unwrap().set_bar(32);
    });
    foo_arc.lock().unwrap().set_bar(31);
    handle.join().unwrap();
}

自行尝试的不安全代码(存在问题)

struct Foo {
    bar: usize,

    // 自定义锁占位
    // lock: std::sync::Mutex<usize>,
}

unsafe impl Sync for Foo {}

impl Foo {
    pub fn set_bar(&mut self, a: usize) {
        self.bar = a;
    }
}

fn main() {
    let mut foo = Foo { bar: 32 };
    foo.bar = 33;

    let foo_arc = std::sync::Arc::new(foo);
    let foo_arc_2 = std::sync::Arc::clone(&foo_arc);

    let handle = std::thread::spawn(move || {
        foo_arc_2.set_bar(32);
    });
    foo_arc.set_bar(31);
    handle.join().unwrap();
}

同时也考虑过是否可以不用Arc,改用更底层的内存操作方式。


解决方案

要实现需求,需要结合Arc提供共享所有权、UnsafeCell提供内部可变性,再配合自定义的同步原语(比如原子变量),手动保证线程安全:

1. 核心逻辑说明

  • Arc负责多线程共享所有权,但内部数据默认不可变,必须用UnsafeCell绕开这个限制,允许内部可变访问。
  • 用原子变量实现自定义同步机制(比如自旋锁),在临界区强制互斥访问,非临界区可直接调用unsafe方法跳过锁检查。
  • 必须严格保证:只有在同步机制保护下才进行可变操作,否则会触发数据竞争和未定义行为。

2. 完整实现示例

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

struct Foo {
    bar: UnsafeCell<usize>,
    // 自定义自旋锁:false=未锁定,true=已锁定
    lock: AtomicBool,
}

// 手动实现Sync:我们通过原子锁保证了线程安全
unsafe impl Sync for Foo {}
// Send自动推导,因为所有成员都实现Send

impl Foo {
    pub fn new(initial_bar: usize) -> Self {
        Foo {
            bar: UnsafeCell::new(initial_bar),
            lock: AtomicBool::new(false),
        }
    }

    // 带锁的可变操作:用于临界区
    pub fn set_bar(&self, a: usize) {
        // 自旋等待锁释放
        while self.lock.compare_exchange(false, true, Ordering::Acquire, Ordering::Relaxed).is_err() {}
        
        // 进入临界区,安全访问UnsafeCell内部数据
        unsafe { *self.bar.get() = a; }
        
        // 释放锁,保证写入操作对其他线程可见
        self.lock.store(false, Ordering::Release);
    }

    // 无锁可变操作:仅当确定无其他线程访问时调用(unsafe)
    pub unsafe fn set_bar_unchecked(&self, a: usize) {
        *self.bar.get() = a;
    }

    // 读取操作:保证读取到最新值
    pub fn get_bar(&self) -> usize {
        // 等待锁释放,确保读取前所有写入完成
        while self.lock.load(Ordering::Relaxed) {}
        unsafe { *self.bar.get() }
    }
}

fn main() {
    let foo = Foo::new(32);
    let foo_arc = Arc::new(foo);
    let foo_arc_2 = Arc::clone(&foo_arc);

    let handle = std::thread::spawn(move || {
        foo_arc_2.set_bar(32);
        // 若确定此时无其他线程操作,可调用unsafe方法
        // unsafe { foo_arc_2.set_bar_unchecked(33); }
    });

    foo_arc.set_bar(31);
    handle.join().unwrap();

    println!("bar: {}", foo_arc.get_bar());
}

3. 不用Arc的底层指针实现

如果不想依赖Arc,可以手动管理堆内存,用原始指针在线程间传递,风险更高但更底层:

use std::sync::atomic::{AtomicBool, Ordering};
use std::thread;
use std::alloc::{alloc, dealloc, Layout};

struct Foo {
    bar: usize,
    lock: AtomicBool,
}

unsafe impl Sync for Foo {}

impl Foo {
    pub fn set_bar(&mut self, a: usize) {
        while self.lock.compare_exchange(false, true, Ordering::Acquire, Ordering::Relaxed).is_err() {}
        self.bar = a;
        self.lock.store(false, Ordering::Release);
    }
}

fn main() {
    // 手动分配堆内存
    let layout = Layout::new::<Foo>();
    let foo_ptr = unsafe { alloc(layout) as *mut Foo };
    unsafe { (*foo_ptr) = Foo { bar: 32, lock: AtomicBool::new(false) }; }

    // 传递原始指针到子线程
    let foo_ptr_2 = foo_ptr;
    let handle = thread::spawn(move || {
        unsafe { (*foo_ptr_2).set_bar(32); }
    });

    unsafe { (*foo_ptr).set_bar(31); }
    handle.join().unwrap();

    // 手动释放内存
    unsafe { dealloc(foo_ptr as *mut u8, layout); }
}

关键注意事项

  • 所有可变操作必须在自定义同步机制的保护下执行,否则会引发数据竞争,导致未定义行为。
  • UnsafeCell是Rust中唯一合法的内部可变实现方式,直接跨线程使用&mut会违反Rust的借用规则。
  • 原子操作的内存顺序(Ordering)必须正确选择:Acquire保证后续读取能看到之前的写入,Release保证之前的写入对其他线程可见,错误的内存顺序会导致可见性问题。

内容的提问来源于stack exchange,提问作者javier

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.16 05:01:02