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

Rust: allow multiple threads to modify an image (wrapper of a vector)?

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

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的scoped线程池:

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);
        });
    });
}

这行不通,因为

  1. 我同时有2个&mut img,这是不允许的
  2. "闭包可能比当前函数长寿,但它借用了 v1,它属于当前函数"

所以我的问题是

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

Playground link

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

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

想法是 chunks_mut() 将整个可变切片视为许多独立的 (non-overlapping) 可变 sub-slices。 因此,每个可变的 sub-slice 都可以被一个线程使用,而不用考虑整个切片被许多线程可变地借用(实际上是,但是以 non-overlapping 的方式,所以它是合理的)。

当然这个例子是微不足道的,将图像分割成许多任意non-overlapping区域应该更棘手。

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]);
    }
}

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

首先是比较简单的#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 比较棘手,你必须去不安全的地方(或者找一个可以帮你做的板条箱,但我找到了 none)。我的想法是存储原始指针而不是向量,然后使用 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:

的 non-mutable 引用
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。我认为这个解决方案是memory-safe,只要你添加代码来强制你的视图不重叠并且你不超出每个视图的范围。