【问题标题】:Lockless processing of non overlapping non contiguous indexes by multiple threads in RustRust 中多个线程对非重叠非连续索引的无锁处理
【发布时间】:2021-09-26 14:32:08
【问题描述】:

我正在练习 rust 并决定创建一个 Matrix ops/factorization 项目。

基本上我希望能够在多个线程中处理底层向量。由于我将为每个线程提供非重叠索引(可能是连续的,也可能不是连续的),并且线程将在创建它们的任何函数结束之前加入,因此不需要锁定/同步。

我知道有几个 crate 可以做到这一点,但我想知道是否有一种相对惯用的无 crate 的方式来自己实现它。

我能想到的最好的方法是(稍微简化一下代码):

use std::thread;

//This represents the Matrix
#[derive(Debug, Clone)]
pub struct MainStruct {
    pub data: Vec<f64>,
}
//This is the bit that will be shared by the threads, 
//ideally it should have its lifetime tied to that of MainStruct
//but i have no idea how to make phantomdata work in this case
#[derive(Debug, Clone)]
pub struct SliceTest {
    pub data: Vec<SubSlice>,
}
//This struct is to hide *mut f64 to allow it to be shared to other threads
#[derive(Debug, Clone)]
pub struct SubSlice {
    pub data: *mut f64,
}

impl MainStruct {
    pub fn slice(&mut self) -> (SliceTest, SliceTest) {
        let mut out_vec_odd: Vec<SubSlice> = Vec::new();

        let mut out_vec_even: Vec<SubSlice> = Vec::new();

        unsafe {
            let ptr = self.data.as_mut_ptr();

            for i in 0..self.data.len() {
                let ptr_to_push = ptr.add(i);
                //Non contiguous idxs
                if i % 2 == 0 {
                    out_vec_even.push(SubSlice{data:ptr_to_push});
                } else {
                    out_vec_odd.push(SubSlice{data:ptr_to_push});
                }
            }
        }

        (SliceTest{data: out_vec_even}, SliceTest{data: out_vec_odd})
    }
}

impl SubSlice {
    pub fn set(&self, val: f64) {
        unsafe {*(self.data) = val;}
    }
}
unsafe impl Send for SliceTest {}
unsafe impl Send for SubSlice {}

fn main() {
    let mut maindata = MainStruct {
        data: vec![0.0, 1.0, 2.0, 3.0, 4.0, 5.0],
    };

    let (mut outvec1, mut outvec2) = maindata.slice();
    let mut threads = Vec::new();

    threads.push(
        thread::spawn(move || {
            for i in 0..outvec1.data.len() {
                outvec1.data[i].set(999.9);
            }
        })
    );
    threads.push(
        thread::spawn(move || {
            for i in 0..outvec2.data.len() {
                outvec2.data[i].set(999.9);
            }
        })
    );

    for handles in threads {
        handles.join();
    }

    println!("maindata = {:?}", maindata.data);
}

编辑: 按照下面的 kmdreko 建议,让代码完全按照我想要的方式工作,而不使用不安全的代码,耶!

当然,就性能而言,复制 f64 切片可能比创建可变参考向量更便宜,除非您的结构填充了其他结构而不是 f64

extern crate crossbeam;
use crossbeam::thread;

#[derive(Debug, Clone)]
pub struct Matrix {
    data: Vec<f64>,
    m: usize, //number of rows
    n: usize, //number of cols
}

...

impl Matrix {
    ...
    pub fn get_data_mut(&mut self) -> &mut Vec<f64> {
        &mut self.data
    }

    pub fn calculate_idx(max_cols: usize, i: usize, j: usize) -> usize {
        let actual_idx = j + max_cols * i;
        actual_idx
    }
    //Get individual mutable references for contiguous indexes (rows)
    pub fn get_all_row_slices(&mut self) -> Vec<Vec<&mut f64>> {
        let max_cols = self.max_cols();
        let max_rows = self.max_rows();
        let inner_data = self.get_data_mut().chunks_mut(max_cols);
        let mut out_vec: Vec<Vec<&mut f64>> = Vec::with_capacity(max_rows);

        for chunk in inner_data {
            let row_vec = chunk.iter_mut().collect();
            out_vec.push(row_vec);
        }

        out_vec
    }
    //Get mutable references for disjoint indexes (columns)
    pub fn get_all_col_slices(&mut self) -> Vec<Vec<&mut f64>> {
        let max_cols = self.max_cols();
        let max_rows = self.max_rows();
        let inner_data = self.get_data_mut().chunks_mut(max_cols);
        let mut out_vec: Vec<Vec<&mut f64>> = Vec::with_capacity(max_cols);

        for _ in 0..max_cols {
            out_vec.push(Vec::with_capacity(max_rows));
        }

        let mut inner_idx = 0;

        for chunk in inner_data {
            let row_vec_it = chunk.iter_mut();

            for elem in row_vec_it {
                out_vec[inner_idx].push(elem);
                inner_idx += 1;
            }

            inner_idx = 0;
        }

        out_vec
    }
    ...
}

fn test_multithreading() {
    fn test(in_vec: Vec<&mut f64>) {
        for elem in in_vec {
            *elem = 33.3;
        }
    }

    fn launch_task(mat: &mut Matrix, f: fn(Vec<&mut f64>)) {

        let test_vec = mat.get_all_row_slices();
        thread::scope(|s| {
            for elem in test_vec.into_iter() {
                s.spawn(move |_| {
                        println!("Spawning thread...");
                        f(elem);
                    });
            }
        }).unwrap();
    }

    let rows = 4;
    let cols = 3;
    //new function code omitted, returns Result<Self, MatrixError>
    let mut mat = Matrix::new(rows, cols).unwrap()

    launch_task(&mut mat, test);

    for i in 0..rows {
        for j in 0..cols {
            //Requires index trait implemented for matrix
            assert_eq!(mat[(i, j)], 33.3);
        }
    }
}

【问题讨论】:

  • 您可以(反复)使用split_at_mut 来获得不同的可变切片?
  • 我也想到了这一点,对于行切片,这工作得很好,因为它们是连续的索引,你可以巧妙地分割向量,但对于列,它的不相交索引变得非常麻烦
  • 仅供参考,不同 CPU 内核同时读取和写入高速缓存行的不同字节没有正确性问题,但可能会出现性能问题。特别是“虚假分享”。在 x86 上,您应该检查 machine_clears.memory_ordering 性能事件,以及过多的缓存未命中与代码的单线程版本。 (如果与您的代码正在执行的其他工作相比,内存访问不是超级频繁,那么您可能会很好,并且它可能比您能想到的任何替代方案都更糟糕,例如您必须从中复制的新数据结构。 )
  • 另外,请不要在问题中编辑答案。您的工作版本应作为答案 发布,其他人可以在此与问题分开投票和评论。 (您可以将其标记为已接受,或标记 kmdreko 的答案。)

标签: multithreading rust lock-free


【解决方案1】:

此 API 不完善。由于SliceTestSubSliceMainStruct没有生命周期注解绑定,所以在数据销毁后可以保留它们,如果使用会导致use-after-free错误。

虽然它很容易使其安全;您可以使用 .iter_mut() 来获得对您的元素的不同可变引用:

pub fn slice(&mut self) -> (Vec<&mut f64>, Vec<&mut f64>) {
    let mut out_vec_even = vec![];
    let mut out_vec_odd = vec![];
    
    for (i, item_ref) in self.data.iter_mut().enumerate() {
        if i % 2 == 0 {
            out_vec_even.push(item_ref);
        } else {
            out_vec_odd.push(item_ref);
        }
    }

    (out_vec_even, out_vec_odd)
}

然而,这又暴露了另一个问题:thread::spawn 不能保存对局部变量的引用。允许创建的线程超出它们创建的范围,因此即使您执行了.join() 他们,您也不需要这样做。这也是您原始代码中的一个潜在问题,只是编译器无法发出警告。

没有简单的方法可以解决这个问题。您需要使用非引用方式来使用其他线程上的数据,但这将使用Arc,它不允许改变其数据,因此您必须求助于Mutex,这是你试图避免的。

我建议从crossbeam 板条箱中获取scope,它确实允许您生成引用本地数据的线程。我知道你想避免使用 crates,但我认为这是最好的解决方案。

playground 上查看工作版本。

见:

【讨论】:

  • thread::spawn 不能保存对局部变量的引用 - 你是说即使这个用例确实 使用.join(),那就是Rust 中的这个特定用例仍然存在问题吗? (在 C++ 中使用指向本地的指针不会有问题,但我只知道 Rust 的零碎部分)。或者你只是在谈论 API 设计,如果他们不.join(),用户可以轻松地在脚上开枪?
  • @PeterCordes 因为线程是否可以超出本地范围取决于是否调用了.join(),并且编译器不会为您检查,所以thread::spawn的API有以防后者。它通过约束要生成的函数是 'static 来做到这一点,这意味着不能保存本地(即非静态)引用。如果你总是调用.join(),使用指针来解决这个问题仍然是合理的,但当然你必须使用unsafe 来使用指针,因为你已经摆脱了 Rust 的安全机制。
  • 啊,我明白了,谢谢。 thread::spawn 实际上不能保存本地引用,并不是说您不应该那样编码,因为 Rust 是一种内存安全语言(与 C++ 不同),这就是它们如何关闭潜在漏洞以防止悬空参考文献。
猜你喜欢
  • 2021-03-05
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-06-17
  • 2022-08-23
  • 1970-01-01
  • 2020-05-11
  • 2015-11-06
相关资源
最近更新 更多