简体   繁体   English

Rust - 在线程之间不安全地共享没有互斥锁的可变数据

[英]Rust - unsafely share between threads a mutable data without mutex

How can old school multi-threading (no wrapping mutex) can be achieve in Rust?如何在 Rust 中实现旧式多线程(无包装互斥量)? And why is it undefined behavior?为什么它是未定义的行为?

I have to build a highly concurrent physic simulation.我必须构建一个高度并发的物理模拟。 I am supposed to do it in C, but I chose Rust (I really needed higher level features).我应该在 C 中完成,但我选择了 Rust(我真的需要更高级别的功能)。

By using Rust, I should opt for a safe communication between threads, however, I must use a mutable buffer shared between the threads .通过使用 Rust,我应该选择线程之间的安全通信,但是,我必须使用线程之间共享的可变缓冲区 (actually, I have to implement different techniques and benchmark them) (实际上,我必须实施不同的技术并对它们进行基准测试)

First approach第一种方法

  1. Use Arc<Data> to share non-mutable state.使用Arc<Data>共享不可变的 state。

  2. Use transmute to promote & to &mut when needed.在需要时使用transmute&提升为&mut

It was straightforward but the compiler would prevent this from compiling even with unsafe block.这很简单,但即使使用unsafe块,编译器也会阻止它编译。 It is because the compiler can apply optimizations knowing this data is supposedly non-mutable (maybe cached and never updated, not an expert about that).这是因为编译器可以应用优化,知道这些数据应该是不可变的(可能被缓存并且从不更新,不是这方面的专家)。

This kind of optimizations can be stopped by Cell wrapper and others.这种优化可以被Cell wrapper 和其他人阻止。

Second approach第二种方法

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

  2. Then data.get() to access data.然后data.get()访问数据。

This does not compile either.这也不编译。 The reason is that UnsafeCell is not Send .原因是UnsafeCell不是Send The solution is to use SyncUnsafeCell but it is unstable for the moment (1.66), and the program will be compile and put to production on a machine with only the stable version.解决方法是使用SyncUnsafeCell ,但目前(1.66)不稳定,程序会在只有稳定版的机器上编译上线。

Third approach第三种方法

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

  2. At the beginning of each threads:在每个线程的开头:

    • Lock the mutex.锁定互斥量。

    • Keep a *mut by coercing a &mut .通过强制&mut来保留*mut mut 。

    • Release the mutex.释放互斥体。

  3. Use the *mut when needed需要时使用*mut

I haven't tried this one yet, but even if it compiles, is it safe (not talking about data race) as it would be with SyncUnsafeCell ?我还没有尝试过这个,但是即使它编译了,它是否像SyncUnsafeCell一样安全(不是谈论数据竞争)?

PS: The values concurrently mutated are just f32 , there are absolutely no memory allocation or any complex operations happening concurrently. PS:并发突变的值只是f32 ,绝对没有 memory 分配或任何同时发生的复杂操作。 Worst case scenario, I have scrambled some f32 .最坏的情况,我加扰了一些f32

Disclaimer: There are probably many ways to solve this, this is just one of them, based on the idea of @Caesar.免责声明:可能有很多方法可以解决这个问题,这只是其中一种,基于@Caesar 的想法。

Two main points of this post:这篇文章的两个要点:

  • You can use AtomicU32 to share f32 between threads without any performance penalty (given an architecture where u32 is already atomic)您可以使用AtomicU32在线程之间共享f32而不会造成任何性能损失(给定u32已经是原子的架构)
  • You can use std::thread::scope to avoid the overhead of Arc .您可以使用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]

To demonstrate how leightweight this is, 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

Note that while this contains zero undefined behaviour, it still is the programmer's responsibility to avoid read-modify-write race conditions.请注意,虽然这包含零个未定义行为,但程序员仍然有责任避免读取-修改-写入竞争条件。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM