Я полный новичок в Rust, и сейчас я пишу эту параллельную игру жизни Конвея. Сам код работает нормально, но проблема в том, что при использовании нескольких потоков программа становится медленнее (я измеряю скорость программы, считая время, когда планер перемещается из верхнего левого угла в нижний правый угол). Я провел несколько экспериментов, и по мере увеличения количества потоков он становился все медленнее и медленнее. У меня также есть версия 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
}
}
1 ответ
Каждый рабочий поток пытается заблокировать мьютекс сразу после запуска и никогда не освобождает блокировку, пока это не будет сделано. Поскольку только один поток может одновременно удерживать мьютекс, только один поток может работать одновременно.
Вот два способа решения этой проблемы:
Не блокируйте мьютекс до тех пор, пока вам это очень, очень не понадобится. Создайте рабочую область внутри рабочего потока, которая представляет блок, который вы обновляете. Сначала заполните область нуля. Затем заблокируйте мьютекс, скопируйте содержимое рабочей области в
new_board
и вернитесь.Используя этот метод, большую часть работы можно выполнять одновременно, но если все ваши работники закончат примерно в одно и то же время, им все равно придется по очереди помещать все это в
new_board
.Не используйте блокировку вообще: измените тип
self.board
наVec<Vec<AtomicI32>>
(std::sync::atomic::AtomicI32
) и атомарно обновить доску без необходимости получения блокировки.Этот метод может замедлить или не замедлить процесс обновления, возможно, в зависимости от того, какой порядок памяти вы используете¹, но он устраняет конкуренцию за блокировку.
Свободный совет
Не вызывайте переменную
Board
. Соглашение, о котором вас предупреждает компилятор, состоит в том, чтобы давать переменным регистровые имена змейки, но помимо этого это сбивает с толку, потому что у вас также есть тип с именемBoard
. Я предлагаю просто назвать егоself
, что также позволяет вам вызыватьupdate
с синтаксисом метода.Не помещайте всю доску в
Arc
, чтобы вы могли передать ееupdate
, а затем создать новую доску, которая должна быть помещена в новуюArc
на следующей итерации. Либо заставьтеupdate
возвращатьArc
сам, либо пусть он принимаетself
и выполняет всюArc
-обработку внутри него.А еще лучше вообще не использовать
Arc
. Используйте ящик с областью threads для передачи ваших данных в рабочие потоки по ссылке.Производительность распределителя, как правило, будет лучше при нескольких больших выделениях, чем при большом количестве маленьких. Измените тип
Board.board
наVec<i32>
и используйте арифметику для вычисления индексов (например, точкаi, j
имеет индексj*n + i
).Также лучше не создавать и выбрасывать выделения, если вам это не нужно. Типичный совет для клеточных автоматов — создать два буфера, содержащих состояния доски: текущее состояние и следующее состояние. Когда вы закончите создание следующего состояния, просто поменяйте местами буферы, чтобы текущее состояние стало следующим состоянием, и наоборот.
i32
тратит место впустую; вы можете использоватьi8
илиenum
, или, возможно,bool
.
¹ Я бы посоветовал SeqCst
, если только вы действительно не понимаете, что делаете. Я подозреваю, что Relaxed
, вероятно, достаточно, но я не совсем понимаю, что делаю.
Похожие вопросы
Связанные вопросы
Новые вопросы
multithreading
Для вопросов, касающихся многопоточности, способность компьютера или программы выполнять работу одновременно или асинхронно, используя несколько одновременных потоков выполнения (обычно называемых потоками).
loop
, поэтому только один поток может выполнять работу одновременно в вашем методеupdate()
, поэтому ваша многопоточность фактически является однопоточной. Тем не менее, вы все равно платите за создание/уничтожение/синхронизацию других потоков.config.rs
, и он не минимален, поскольку содержит много лишнего кода (анализ аргументов, рендеринг и т. д.). .), что не нужно наблюдать за плохим поведением. Вы можете заменитьloop
вmain
просто наfor _ in 0..100 { }
и не поместить в него ничего, кроме вызоваupdate
, например, и определитьconst
ant для размера доски и количество потоков, вместо того, чтобы анализировать их из командной строки.