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

问题描述

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

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,}

impl<'a> RawImageView<'a>
{
    pub fn new(img: &'a mut RawImage,x0: u32,y0: u32,height: u32) -> Self
    {
        Self{ img: img,offset_x: x0,offset_y: y0,height: height,}
    }

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

现在假设我有一个图像,并且我想有2个线程同时修改它。在这里,我使用人造丝的作用域线程池:

fn modify(img: &mut RawImageView)
{
    // Do some heavy calculation and write to the image.
    img.set_pixel(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,10,10);
        let mut v2 = RawImageView::new(&mut img,10);
        s.spawn(|_| {
            modify(&mut v1);
        });
        s.spawn(|_| {
            modify(&mut v2);
        });
    });
}

这不起作用,因为

  1. 我同时有2个&mut img,这是不允许的
  2. 关闭可能会超出当前功能,但它借用了v1,它由当前功能拥有”

所以我的问题是

  1. 如何修改RawImageView,以便有2个线程可以修改图像?
  2. 即使线程是作用域的,为什么它仍然抱怨关闭的生命周期?而我该如何克服呢?

Playground link

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

解决方法

您的两个问题实际上无关。

首先使用#2 更容易:

Rayon作用域线程的想法是,在内部创建的线程不能超过范围,因此可以安全地借用在 outside 范围内创建的任何变量,并将其引用发送到线程中。但是您的变量是在范围内 创建的,因此您一无所获。

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

    let mut v1 = RawImageView::new(&mut img,10,10);
    let mut v2 = RawImageView::new(&mut img,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],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,}

并使用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。我认为该解决方案是内存安全的,只要您添加代码以强制您的视图不重叠并且不超出每个视图的范围即可。

,

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

这个想法是,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>,}

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

    fn xy2index(
        &self,) -> 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],}

impl<'a> RawImagePart<'a> {
    pub fn set_pixel(
        &mut self,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,[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]);
    }
}