3

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

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

这不起作用,因为

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

所以我的问题是

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

游乐场链接

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

4

2 回答 2

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<'_> {}

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

于 2020-10-09T07:37:49.333 回答
0

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

这个想法是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]);
    }
}
于 2020-10-09T07:37:43.457 回答