在不安全的代码中,对同一个数组有多个可变引用(不是指针)是否正确,只要它们不用于写入相同的索引?
语境
我想产生一个底层数组的几个(不同的)可变视图,我可以从不同的线程进行修改。
如果不相交的部分是连续的,这很简单,只需调用split_at_mut
切片即可:
let mut v = [1, 2, 3, 4];
{
let (left, right) = v.split_at_mut(2);
left[0] = 5;
right[0] = 6;
}
assert!(v == [5, 2, 6, 4]);
但我也想暴露不连续的不相交部分。为简单起见,假设我们要检索偶数索引的可变“视图”,以及奇数索引的另一个可变“视图”。
与 相反split_at_mut()
,我们无法检索两个可变引用(我们想要一个安全的抽象!),所以我们使用两个结构实例,只公开对偶数(或奇数)索引的可变访问:
let data = &mut [0i32; 11];
let (mut even, mut odd) = split_fields(data);
// …
使用一些不安全的代码,很容易得到这样一个安全的抽象。这是一个可能的实现:
use std::marker::PhantomData;
struct SliceField<'a> {
ptr: *mut i32,
len: usize,
field: usize,
marker: PhantomData<&'a mut i32>,
}
impl SliceField<'_> {
fn inc(&mut self) {
unsafe {
for i in (self.field..self.len).step_by(2) {
*self.ptr.add(i) += 1;
}
}
}
fn dec(&mut self) {
unsafe {
for i in (self.field..self.len).step_by(2) {
*self.ptr.add(i) -= 1;
}
}
}
}
unsafe impl Send for SliceField<'_> {}
fn split_fields(array: &mut [i32]) -> (SliceField<'_>, SliceField<'_>) {
(
SliceField {
ptr: array.as_mut_ptr(),
len: array.len(),
field: 0,
marker: PhantomData,
},
SliceField {
ptr: array.as_mut_ptr(),
len: array.len(),
field: 1,
marker: PhantomData,
},
)
}
fn main() {
let data = &mut [0i32; 11];
{
let (mut even, mut odd) = split_fields(data);
rayon::join(|| even.inc(), || odd.dec());
}
// this prints [1, -1, 1, -1, 1, -1, 1, -1, 1, -1, 1]
println!("{:?}", data);
}
到现在为止还挺好。
问题
但是,访问原始指针非常方便:与切片相反,我们不能使用运算符[]
或迭代器。
unsafe {
for i in (self.field..self.len).step_by(2) {
*self.ptr.add(i) += 1;
}
}
显而易见的想法是将原始指针本地转换为不安全实现中的切片:
let slice = unsafe { slice::from_raw_parts_mut(self.ptr, self.len) };
然后我们可以,例如,以函数式风格重写我们的实现:
slice.iter_mut().skip(self.field).step_by(2).for_each(|x| *x += 1);
对于这个示例,它可能不值得,但对于更复杂的代码,使用切片而不是原始指针可能更方便。
问题
它是否正确?
这显然违反了借用规则:两个线程可能同时持有对同一内存位置的可变引用。但是,它们可能永远不会写入相同的索引。
可变引用别名没有被列为不安全的超级大国,但该列表并不详尽。