[英]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第一种方法
Use Arc<Data>
to share non-mutable state.使用Arc<Data>
共享不可变的 state。
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第二种方法
Use Arc<UnsafeCell<Data>>
.使用Arc<UnsafeCell<Data>>
。
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第三种方法
Use Arc<Mutex<Data>>
.使用Arc<Mutex<Data>>
。
At the beginning of each threads:在每个线程的开头:
Lock the mutex.锁定互斥量。
Keep a *mut
by coercing a &mut
.通过强制&mut
来保留*mut
mut 。
Release the mutex.释放互斥体。
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:这篇文章的两个要点:
AtomicU32
to share f32
between threads without any performance penalty (given an architecture where u32
is already atomic)您可以使用AtomicU32
在线程之间共享f32
而不会造成任何性能损失(给定u32
已经是原子的架构)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.