我是Rust的入门者,目前正在撰写平行的Conway的生活游戏。代码本身可以正常工作,但是问题是当使用多个线程时,程序会变慢(我通过计算滑翔器从左上角到右下角的移动时间来测量程序的速度)。我做了一些实验,随着线程数量的增加,它变得越来越慢。我也有使用几乎相同算法的Java版本。它工作正常。我所期望的是,Rust版本在线程数超过一个的情况下至少可以变得更快一点。有人可以指出我做错了什么吗?很抱歉,如果代码看起来不合理,就像我说的我是一个完整的初学者:-)。
main.rs读取命令行参数并进行主板更新。
extern crate clap;
extern crate termion;
extern crate chrono;
use std::thread;
use std::sync::Arc;
use chrono::prelude::*;
mod board;
mod config;
use board::Board;
use config::Config;
fn main() {
let dt1 = Local::now();
let matches = clap::App::new("conway")
.arg(clap::Arg::with_name("length")
.long("length")
.value_name("LENGTH")
.help("Set length of the board")
.takes_value(true))
.arg(clap::Arg::with_name("threads")
.long("threads")
.value_name("THREADS")
.help("How many threads to update the board")
.takes_value(true))
.arg(clap::Arg::with_name("display")
.long("display")
.value_name("DISPLAY")
.help("Display the board or not")
.takes_value(true))
.arg(clap::Arg::with_name("delay")
.long("delay")
.value_name("MILLISECONDS")
.help("Delay between the frames in milliseconds")
.takes_value(true))
.get_matches();
let config = Config::from_matches(matches);
let mut board = Board::new(config.length);
let mut start: bool = false;
let mut end: bool = false;
let mut start_time: DateTime<Local> = Local::now();
let mut end_time: DateTime<Local>;
board.initialize_glider();
loop {
if config.display == 1 {
print!("{}{}", termion::clear::All, termion::cursor::Goto(3, 3));
board_render(&board);
}
if board.board[0][1] == 1 && !start {
start_time = Local::now();
start = true;
}
if board.board[config.length - 1][config.length - 1] == 1 && !end {
end_time = Local::now();
println!("{}", end_time - start_time);
end = true;
}
board = board::Board::update(Arc::new(board), config.threads);
thread::sleep(config.delay);
}
}
fn board_render(board: &Board) {
let mut output = String::with_capacity(board.n * (board.n + 1));
for i in 0..board.n {
for j in 0..board.n {
let ch;
if board.board[i][j] == 0 {
ch = '░';
} else {
ch = '█';
}
output.push(ch);
}
output.push_str("\n ");
}
print!("{}", output);
}
board.rs是用于更新具有多个线程的电路板的算法
use std::sync::{Mutex, Arc};
use std::thread;
pub struct Board {
pub n: usize,
pub board: Vec<Vec<i32>>,
}
impl Board {
pub fn new(n: usize) -> Board {
let board = vec![vec![0; n]; n];
Board {
n,
board,
}
}
pub fn update(Board: Arc<Self>, t_num: usize) -> Board {
let new_board = Arc::new(Mutex::new(Board::new(Board.n)));
let mut workers = Vec::with_capacity(t_num);
let block_size = Board.n / t_num;
let mut start = 0;
for t in 0..t_num {
let old_board = Board.clone();
let new_board = Arc::clone(&new_board);
let mut end = start + block_size;
if t == t_num - 1 { end = old_board.n; }
let worker = thread::spawn(move || {
let mut board = new_board.lock().unwrap();
for i in start..end {
for j in 0..old_board.n {
let im = (i + old_board.n - 1) % old_board.n;
let ip = (i + 1) % old_board.n;
let jm = (j + old_board.n - 1) % old_board.n;
let jp = (j + 1) % old_board.n;
let sum = old_board.board[im][jm] + old_board.board[im][j]
+ old_board.board[im][jp] + old_board.board[i][jm] + old_board.board[i][jp]
+ old_board.board[ip][jm] + old_board.board[ip][j] + old_board.board[ip][jp];
if sum == 2 {
board.board[i][j] = old_board.board[i][j];
} else if sum == 3 {
board.board[i][j] = 1;
} else {
board.board[i][j] = 0;
}
}
}
});
workers.push(worker);
start = start + block_size;
}
for worker in workers {
worker.join().unwrap();
}
let result = new_board.lock().unwrap();
let mut board = Board::new(Board.n);
board.board = result.board.to_vec();
board
}
pub fn initialize_glider(&mut self) -> &mut Board {
self.board[0][1] = 1;
self.board[1][2] = 1;
self.board[2][0] = 1;
self.board[2][1] = 1;
self.board[2][2] = 1;
self
}
}
答案 0 :(得分:3)
每个工作线程在启动时都会尝试立即锁定互斥锁,直到完成后才释放锁。由于一次只有一个线程可以容纳该互斥锁,因此一次只能有一个线程可以工作。
有两种方法可以解决此问题:
在您确实需要之前,不要锁定互斥锁。在辅助线程中创建一个临时区域,该区域代表您要更新的块。首先填充划痕区域。 然后锁定互斥锁,将暂存区域的内容复制到new_board
中,然后返回。
使用此方法,大多数工作可以同时完成,但是如果您所有的工人都在大致相同的时间完成工作,他们仍然必须轮流将其全部放入new_board
。
完全不使用锁定:将self.board
的类型更改为Vec<Vec<AtomicI32>>
(std::sync::atomic::AtomicI32
),并在无需获取锁定的情况下自动更新板。 / p>
此方法可能会或可能不会减慢更新过程,这可能取决于您使用的内存顺序¹,但它消除了锁争用。
不要调用变量Board
。编译器提醒您的约定是为变量赋予蛇形名称,但除此之外,它令人困惑,因为您还有一个名为Board
的类型。我建议实际上只是调用它self
,这也可以让您使用方法语法来调用update
。
不要将整个电路板放在Arc
中,因此您可以将其传递给update
,然后制作一个新的电路板,而必须将其放置在新的{{1}中下一次迭代。让Arc
本身返回一个update
,或者让它花费Arc
并在其中进行所有的self
纠缠。
更好的是,根本不要使用Arc
。使用提供scoped threads的板条箱通过引用将您的数据传递到辅助线程。
分配器的性能通常会好于一些大分配,而不是许多小分配。将Arc
的类型更改为Board.board
并使用算术来计算索引(例如,点Vec<i32>
位于索引i, j
上。)
如果不需要,最好不要创建并丢弃分配。细胞自动机的典型建议是创建两个包含板状态的缓冲区:当前状态和下一个状态。创建完下一个状态后,只需交换缓冲区,以便当前状态变为下一个状态,反之亦然。
j*n + i
浪费了空间;您可以使用i32
或i8
,也可以使用enum
。
¹我建议您bool
,除非您真的知道自己在做什么。我怀疑SeqCst
可能就足够了,但是我不真的不知道我在做什么。