如何在Rust中实现老式多线程(无包装互斥)?为什么它是未定义的行为?

我必须建立一个高度并行的物理模拟.我应该用C语言来做这件事,但我 Select 了Rust(我确实需要更高级的特性).

通过使用Rust,我应该 Select 线程之间的安全通信,但是,我必须使用mutable buffer shared between the threads.(实际上,我必须实现不同的技术并对它们进行基准测试)

First approach

  1. 使用Arc<Data>共享不可变状态.

  2. 如果需要,使用transmute&提升到&mut.

这很简单,但即使有unsafe个块,编译器也会阻止编译.这是因为编译器可以在知道这些数据应该是不可变的情况下应用优化(可能缓存且从不更新,而不是这方面的专家).

这种优化可以被Cell包装器和其他包装器阻止.

Second approach

  1. 使用Arc<UnsafeCell<Data>>.

  2. 然后是data.get()来访问数据.

这也不能编译.原因是UnsafeCell不是Send.解决方案是使用SyncUnsafeCell,但目前它不稳定(1.66),程序将在只有稳定版本的机器上编译并投入生产.

Third approach

  1. 使用Arc<Mutex<Data>>.

  2. 在每个线程的开头:

    • 锁定互斥体.

    • 通过强迫得&mut分来保持*mut分.

    • 释放互斥体.

  3. 在需要时使用*mut

我还没有试过这个,但即使它编译了,它是否像SyncUnsafeCell一样安全(不是在谈论数据竞赛)?

PS:并发改变的值只有f32,绝对没有内存分配或任何复杂的操作并发发生.最坏的情况是,我已经紧急安排了大约f32架.

推荐答案

免责声明:可能有很多方法来解决这个问题,这只是其中之一,基于@Caesar的 idea .

这篇文章的两个要点:

  • 您可以使用AtomicU32在线程之间共享f32,而不会造成任何性能损失(给定u32已经是原子的体系 struct )
  • 你可以用std::thread::scope来避免Arc的开销.
use std::{
    fmt::Debug,
    ops::Range,
    sync::atomic::{AtomicU32, Ordering},
};

struct AtomicF32(AtomicU32);
impl AtomicF32 {
    pub fn new(val: f32) -> Self {
        Self(AtomicU32::new(val.to_bits()))
    }
    pub fn load(&self, order: Ordering) -> f32 {
        f32::from_bits(self.0.load(order))
    }
    pub fn store(&self, val: f32, order: Ordering) {
        self.0.store(val.to_bits(), order)
    }
}
impl Debug for AtomicF32 {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        self.load(Ordering::Relaxed).fmt(f)
    }
}

fn perform_action(data: &Vec<AtomicF32>, range: Range<usize>) {
    for value_raw in &data[range] {
        let mut value = value_raw.load(Ordering::Relaxed);
        value *= 2.5;
        value_raw.store(value, Ordering::Relaxed);
    }
}

fn main() {
    let data = (1..=10)
        .map(|v| AtomicF32::new(v as f32))
        .collect::<Vec<_>>();

    println!("Before: {:?}", data);

    std::thread::scope(|s| {
        s.spawn(|| perform_action(&data, 0..5));
        s.spawn(|| perform_action(&data, 5..10));
    });

    println!("After: {:?}", data);
}
Before: [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0]
After: [2.5, 5.0, 7.5, 10.0, 12.5, 15.0, 17.5, 20.0, 22.5, 25.0]

要演示这是多么轻量级,请看here is what this compiles to:

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

pub struct AtomicF32(AtomicU32);
impl AtomicF32 {
    fn load(&self, order: Ordering) -> f32 {
        f32::from_bits(self.0.load(order))
    }
    fn store(&self, val: f32, order: Ordering) {
        self.0.store(val.to_bits(), order)
    }
}

pub fn perform_action(value_raw: &AtomicF32) {
    let mut value = value_raw.load(Ordering::Relaxed);
    value *= 2.5;
    value_raw.store(value, Ordering::Relaxed);
}
.LCPI0_0:
        .long   0x40200000
example::perform_action:
        movss   xmm0, dword ptr [rdi]
        mulss   xmm0, dword ptr [rip + .LCPI0_0]
        movss   dword ptr [rdi], xmm0
        ret

请注意,虽然这包含零个未定义的行为,但避免读-修改-写争用条件仍然是程序员的责任.

Rust相关问答推荐

什么样的 struct 可以避免使用RefCell?

如何在Rust中实现Functor trait?

带扫描的铁 rust 使用滤镜

如何提高自定义迭代器的`extend`性能

如何正确地将App handler传递给Tauri中的其他模块?

交换引用时的生命周期

Rust&;Tokio:如何处理更多的信号,而不仅仅是SIGINT,即SIGQUE?

如果死 struct 实现了/派生了一些特征,为什么Rust会停止检测它们?

当发送方分配给静态时,Tokio MPSC关闭通道

如何实现Deref;多次;?

为什么rustc会自动降级其版本?

如何将 struct 数组放置在另一个 struct 的末尾而不进行内存分段

Rust Axum 框架 - 解包安全吗?

可以在旋转循环中调用try_recv()吗?

是否可以在 Rust 中的特定字符上实现特征?

为什么指定生命周期让我返回一个引用?

Rust,我如何正确释放堆分配的内存?

如何为枚举中的单个或多个值返回迭代器

提取 struct 生成宏中字段出现的索引

在传输不可复制的值时实现就地枚举修改