Rust:允许多个线程修改图像(向量的包装器)?

Posted

技术标签:

【中文标题】Rust:允许多个线程修改图像(向量的包装器)?【英文标题】:Rust: allow multiple threads to modify an image (wrapper of a vector)? 【发布时间】:2021-01-24 05:38:44 【问题描述】:

假设我有一个包装矢量的“图像”结构:

type Color = [f64; 3];

pub struct RawImage

    data: Vec<Color>,
    width: u32,
    height: u32,


impl RawImage

    pub fn new(width: u32, height: u32) -> Self
    
        Self 
            data: vec![[0.0, 0.0, 0.0]; (width * height) as usize],
            width: width,
            height: height
        
    

    fn xy2index(&self, x: u32, y: u32) -> usize
    
        (y * self.width + x) as usize
    

它可以通过“视图”结构访问,该结构抽象了图像的内部块。假设我只想写入图像 (set_pixel())。

pub struct RawImageView<'a>

    img: &'a mut RawImage,
    offset_x: u32,
    offset_y: u32,
    width: u32,
    height: u32,


impl<'a> RawImageView<'a>

    pub fn new(img: &'a mut RawImage, x0: u32, y0: u32, width: u32, height: u32) -> Self
    
        Self img: img,
              offset_x: x0, offset_y: y0,
              width: width, height: height, 
    

    pub fn set_pixel(&mut self, x: u32, y: u32, color: Color)
    
        let index = self.img.xy2index(x + self.offset_x, y + self.offset_y);
        self.img.data[index] = color;
    

现在假设我有一个图像,我希望有 2 个线程同时修改它。这里我使用了 rayon 的作用域线程池:

fn modify(img: &mut RawImageView)

    // Do some heavy calculation and write to the image.
    img.set_pixel(0, 0, [0.1, 0.2, 0.3]);


fn main()

    let mut img = RawImage::new(20, 10);
    let pool = rayon::ThreadPoolBuilder::new().num_threads(2).build().unwrap();
    pool.scope(|s| 
        let mut v1 = RawImageView::new(&mut img, 0, 0, 10, 10);
        let mut v2 = RawImageView::new(&mut img, 10, 0, 10, 10);
        s.spawn(|_| 
            modify(&mut v1);
        );
        s.spawn(|_| 
            modify(&mut v2);
        );
    );

这行不通,因为

    我同时有2个&amp;mut img,这是不允许的 “闭包的寿命可能比当前函数长,但它借用了当前函数所拥有的v1

所以我的问题是

    如何修改RawImageView,让2个线程修改我的图片? 为什么它仍然抱怨闭包的生命周期,即使线程是作用域的?我该如何克服呢?

Playground link

我尝试过的一种方法(并且有效)是让modify() 创建并返回一个RawImage,然后让线程将其推入向量中。完成所有线程后,我从该向量构造了完整图像。由于它的 RAM 使用率,我试图避免这种方法。

【问题讨论】:

【参考方案1】:

你的两个问题实际上是无关的。

首先是比较简单的#2

Rayon 作用域线程的想法是,在内部创建的线程不能超过作用域,因此在作用域外部创建的任何变量都可以安全地借用,并将其引用发送到线程中。但是您的变量是在 范围内创建的,这对您没有任何好处。

解决方案很简单:将变量移出范围:

    let mut v1 = RawImageView::new(&mut img, 0, 0, 10, 10);
    let mut v2 = RawImageView::new(&mut img, 10, 0, 10, 10);
    pool.scope(|s| 
        s.spawn(|_| 
            modify(&mut v1);
        );
        s.spawn(|_| 
            modify(&mut v2);
        );
    );

#1 比较棘手,你必须不安全(或者找一个可以为你做这件事的板条箱,但我没有找到)。我的想法是存储原始指针而不是向量,然后使用std::ptr::write 写入像素。如果您仔细地执行此操作并添加自己的边界检查,它应该是非常安全的。

我将添加一个额外的间接级别,也许你可以只使用两个,但这会保留更多的原始代码。

RawImage 可能类似于:

pub struct RawImage<'a>

    _pd: PhantomData<&'a mut Color>,
    data: *mut Color,
    width: u32,
    height: u32,

impl<'a> RawImage<'a>

    pub fn new(data: &'a mut [Color], width: u32, height: u32) -> Self
    
        Self 
            _pd: PhantomData,
            data: data.as_mut_ptr(),
            width: width,
            height: height
        
    

然后构建图像,将像素保持在外面:

    let mut pixels = vec![[0.0, 0.0, 0.0]; (20 * 10) as usize];
    let mut img = RawImage::new(&mut pixels, 20, 10);

现在RawImageView 可以保留对RawImage 的不可变引用:

pub struct RawImageView<'a>

    img: &'a RawImage<'a>,
    offset_x: u32,
    offset_y: u32,
    width: u32,
    height: u32,

并使用ptr::write 写入像素:

    pub fn set_pixel(&mut self, x: u32, y: u32, color: Color)
    
        let index = self.img.xy2index(x + self.offset_x, y + self.offset_y);
        //TODO! missing check bounds
        unsafe  self.img.data.add(index).write(color) ;
    

但不要忘记在这里检查边界或将此功能标记为不安全,将责任交给用户。

自然,由于您的函数保留对可变指针的引用,因此不能在线程之间发送。但我们更清楚:

unsafe impl Send for RawImageView<'_> 

就是这样! Playground。我认为这个解决方案是内存安全的,只要你添加代码来强制你的视图不重叠并且你不会超出每个视图的范围。

【讨论】:

感谢您的解决方案!很有意思!我尝试使用原始指针,但我把它放在RawImageView 中。它不起作用,因为原始指针不是同步和发送。但我从没想过让RawImage拥有指针。 @MetroWind:实际上我认为您可以将原始指针添加到RawImageView 并使其工作,无论如何我必须手动实现Send。我这样做是因为您的xy2indexRawImage 中,但可以轻松移动。 « 我认为这个解决方案是内存安全的,只要你的视图不重叠 » 所以这绝对是安全的,或者它和 C 一样安全, C++... Rust 中安全性背后的理念是 语言本身 保证您不能同时对同一数据块进行写入或读写访问(大部分时间在编译和有时在运行时)。在这个建议的解决方案中,这是它的用户(甚至不是它的设计者)的责任。在我看来,它就像是 Rust 关注点中反模式的精确说明。 @prog-fh:你是对的,当然。我的意思是这个解决方案是内存安全的......只要你添加必要的代码以确保视图不重叠,以及确保像素不会消失的代码边界...此代码只是工作解决方案的草图,而不是最终代码。 @prog-fh 刚刚看到你的评论。你是绝对正确的,因为它和 C/C++ 一样安全。然而,在数值计算中绝对有必要能够以任意方式在逻辑上破坏一块连续内存,而无需复制,并同时写入它们。人们可以把它藏在图书馆里,假装它是“安全的”,但关键字是“假装”。如果这是一个反模式,至少它是一个必要的反模式。【参考方案2】:

这与您的图像问题不完全匹配,但这可能会给您一些线索。

这个想法是chunks_mut() 将整个可变切片视为许多独立(不重叠)的可变子切片。 因此,每个可变子切片都可以被一个线程使用,而无需考虑整个切片被许多线程可变地借用(实际上是这样,但以非重叠的方式,所以它是合理的)。

当然,这个例子很简单,将图像分割成许多任意不重叠的区域应该会更棘手。

fn modify(
    id: usize,
    values: &mut [usize],
) 
    for v in values.iter_mut() 
        *v += 1000 * (id + 1);
    


fn main() 
    let mut values: Vec<_> = (0..8_usize).map(|i| i + 1).collect();

    let pool = rayon::ThreadPoolBuilder::new()
        .num_threads(2)
        .build()
        .unwrap();
    pool.scope(|s| 
        for (id, ch) in values.chunks_mut(4).enumerate() 
            s.spawn(move |_| 
                modify(id, ch);
            );
        
    );

    println!(":?", values);


编辑

我不知道在什么情况下需要对图像的某些部分进行并行处理,但我可以想象两种情况。

如果目的是处理图像的任意部分并允许这发生在计算许多其他事物的多个线程中,那么一个简单的互斥体来规范对全局图像的访问当然就足够了。 确实,如果图像每个部分的精确形状非常重要,那么并行化就不太可能有这么多的形状。

另一方面,如果意图是并行化图像处理以实现高性能,那么每个部分的具体形状可能不那么相关,因为唯一要确保的重要保证是整个图像是当所有线程完成时处理。 在这种情况下,一个简单的一维分裂(沿 y)就足够了。

例如,下面是对原始代码的最小修改,以便将图像自身拆分为多个可变部分,这些部分可以由多个线程安全处理。 不需要不安全的代码,不需要昂贵的运行时检查或副本,这些部分是连续的,因此可高度优化。

type Color = [f64; 3];

pub struct RawImage 
    data: Vec<Color>,
    width: u32,
    height: u32,


impl RawImage 
    pub fn new(
        width: u32,
        height: u32,
    ) -> Self 
        Self 
            data: vec![[0.0, 0.0, 0.0]; (width * height) as usize],
            width: width,
            height: height,
        
    

    fn xy2index(
        &self,
        x: u32,
        y: u32,
    ) -> usize 
        (y * self.width + x) as usize
    

    pub fn mut_parts(
        &mut self,
        count: u32,
    ) -> impl Iterator<Item = RawImagePart> 
        let part_height = (self.height + count - 1) / count;
        let sz = part_height * self.width;
        let width = self.width;
        let mut offset_y = 0;
        self.data.chunks_mut(sz as usize).map(move |part| 
            let height = part.len() as u32 / width;
            let p = RawImagePart 
                part,
                offset_y,
                width,
                height,
            ;
            offset_y += height;
            p
        )
    


pub struct RawImagePart<'a> 
    part: &'a mut [Color],
    offset_y: u32,
    width: u32,
    height: u32,


impl<'a> RawImagePart<'a> 
    pub fn set_pixel(
        &mut self,
        x: u32,
        y: u32,
        color: Color,
    ) 
        let part_index = x + y * self.width;
        self.part[part_index as usize] = color;
    


fn modify(img: &mut RawImagePart) 
    // Do some heavy calculation and write to the image.
    let dummy = img.offset_y as f64 / 100.0;
    let last = img.height - 1;
    for x in 0..img.width 
        img.set_pixel(x, 0, [dummy + 0.1, dummy + 0.2, dummy + 0.3]);
        img.set_pixel(x, last, [dummy + 0.7, dummy + 0.8, dummy + 0.9]);
    


fn main() 
    let mut img = RawImage::new(20, 10);
    let pool = rayon::ThreadPoolBuilder::new()
        .num_threads(2)
        .build()
        .unwrap();
    pool.scope(|s| 
        for mut p in img.mut_parts(2) 
            s.spawn(move |_| 
                modify(&mut p);
            );
        
    );
    for y in 0..img.height 
        let offset = (y * img.width) as usize;
        println!(":.2?...", &img.data[offset..offset + 3]);
    

【讨论】:

感谢您的回答!是的。如果我的数据可以以 1-D 方式拆分,这就是我会做的事情。我想问题是如何实现这个的 2D 版本。

以上是关于Rust:允许多个线程修改图像(向量的包装器)?的主要内容,如果未能解决你的问题,请参考以下文章

抓狂!当 Rust 从 C FFI 调用时,没有产生线程

Rust编程语言入门之无畏并发

需要关于Rust的细胞和参考计数类型的整体解释

我可以为任何可以放入 Extern C 的 C++ 向量制作一个 C 包装器吗

返回一个已知大小的向量而不需要额外的包装器?

无锁定的多个矢量编写器