Looking for the best approach for a parallel loop execution

Hi,

I have a struct TiledSurface that contains a Vec containing structs of SurfaceTile that I want to mutate in a parallel loop, using Rayon.
The only way I could make this work, is by wrapping the tile list inside a Mutex. But this completely destroys the parallel nature of course, since I need to hold the lock for the full duration of tile.draw_quad().

Rust is very new for me and since this is pretty common thing to have in a job based system, I wonder what the best approach for this is in Rust. I know that I am fetching each tile only once, for each task. So this should not need a Mutex at all. Of course the Borrow checker cannot know that, but what would be the correct way (TM) of writing such a setup in Rust?

I have the following setup (I tried to remove all the non important bits, so this only part of my code):

//A task defining a single draw action in a SurfaceTile
pub struct DrawQuadTask<Format> {
    tile_index: usize,
    posx: usize,
    posy: usize,
    width: usize,
    height: usize,
    color: Format,
}

//A tiled surface, contains a list of SurfaceTiles
pub struct TiledSurface<Format> {
    tiles: Vec<SurfaceTile<Format>>,
}

impl<Format> TiledSurface<Format> {

	pub fn fill_quads() {

		//Create list of tasks, to be executed later
		let mut v = Vec::<DrawQuadTask<Format>>::new();

		//Depending on some non important prerequisists, I push new Tasks to v.
		v.push(DrawQuadTask::<Format> {
		    tile_index: index,
		    posx: left,
		    posy: top,
		    width: tile_width,
		    height: tile_height,
		    color: color,
		});

		let tiles = Mutex::new(&mut self.tiles);
		v.as_slice().par_iter().for_each(|task| { //parallel loop
		    let tile = &mut tiles.lock().unwrap()[task.tile_index];
		    tile.draw_quad(task.posx, task.posy, task.width, task.height, task.color);
		});

	}	
}

I tried a whole list of different approaches, using ARC or Cell or RefCell or Box. But nothing can make the Borrow checker happy with this code setup.

So, how should I take this on?

Any input is greatly appreciated :slight_smile:

You are not going to get this to work if you are using the indexing operator a[b] because the borrow checker does not understand what it does and considers it a use of the entire vector. Instead, you must split the vector with par_iter_mut, which provides a sequence of non-overlapping mutable references into the vector. You can also split it with methods like split_at_mut.

It is a bit difficult to do this when you have a tile_index in that manner. If you know that you have all of the tiles exactly once and in order, you can do something like this:

self.tiles.par_iter_mut().zip(v.as_slice().par_iter())
    .for_each(|(tile, task)| {
        tile.draw_quad(task.posx, task.posy, task.width, task.height, task.color);
    });

The use of zip will pair the elements in the iterators in order.

1 Like

You could have every individual SurfaceTile in its own Mutex. (I.e. tiles: Vec<Mutex<SurfaceTile<Format>>>,.) I don’t know how problematic such a change would be for the remainder of your code, but as long as collisions of tile_indexes aren’t too common, it might not kill the benefit from parallelization.

Thanks @alice ! This has put me on the right track.

I have a setup that works without a Mutex now. Though, it's not ideal in my opinion. I now made it so that 'v' and 'self.tiles' are the same length. Like that I can zip-iterate over the two Vecs.
And this works fine! But obviously this is not very optimal if the amount of Quads to draw is very low compared to the total list of tiles. 'v' will contain a lot of 'null' draw tasks, but which need iteration nonetheless.

@steffahn Thanks for the help! But I think this wont help here. The issue here is I need shared mutable access still to the tiles Vec. (Although I only access the individual tiles I still need to iterate mutably over the vector).
And the setup is that I never have shared access to a single tile, I only iterate over the tiles in parallel. So I should not need a Mutex anywhere. Of course, the list would need guarding if the list if mutable from the outsidem but this is not the case here.

If you sort the tasks by tile id, it would be possible to write an iterator that uses split_at_mut to skip forward many items in self.tiles in one go.