I am not sure whether the usage of the word "thread" in the title is correct since I am new to the space of multi threading, async, parallelism or concurrency.
So the last few weeks I have been working on a connect 4 game engine (that displays the gamestate in the console, checks moves for validity, checks if someone won, etc.) some players/bots, namely a human one, where the player can enter which column they wish to play next, a bruteforce one and a montecarlo based one.
Now for montecarlo there are a lot of calculations involved and the more time one gets to simulate games the better the choice of the montecarlo player should be (at least up to some point). And now I want to use the time the human player needs to decide what to do next (in case it is playing against the montecarlo player) for the montecarlo to do calculations, since the program is only waiting on input and doing nothing else.
And here is where I think threads could come in handy. However I am not quite sure of that even. So feel free to tell me if there are better solutions.
use std::thread;
use std::sync::mpsc;
struct Engine {
}
// Engine that plays the game. There are different ones in the actual game. This is for simplicity's sake
impl Engine {
pub fn new() -> Engine {
Engine {}
}
pub fn make_move(&mut self, gamestate: u32) {
println!("Player made move: {}", gamestate + 2);
}
}
fn main() {
let mut player_blue = Engine::new();
let mut player_red = Engine::new();
for _ in 0..2 {
let (tx_blue, rx_blue) = mpsc::channel();
let (tx_blue_back, rx_blue_back) = mpsc::channel();
let manager_blue = thread::spawn(move || {
let mut counter = 0;
loop {
if let Ok(t) = rx_blue.try_recv() {
player_blue.make_move(t);
tx_blue_back.send(counter).unwrap();
break;
}
counter += 1;
}
});
let manager_red = thread::spawn(move || {
let mut input = String::new();
// Reading in input from human
println!("Please enter the important information we need:");
match std::io::stdin().read_line(&mut input) {
Ok(_) => (),
Err(error) => panic!("Problem reading in input: {:?}", error),
}
// Matching the input
let val = match parse_string_tuple(input.trim()) {
Some(i) => i,
None => 0,
};
// Make move
player_red.make_move(val);
// Tell other thread that reading in is finished and important calculations should be stopped
tx_blue.send(0).unwrap();
});
manager_red.join().unwrap();
manager_blue.join().unwrap();
let counter = rx_blue_back.recv().unwrap();
println!("Counter is: {}", counter);
}
println!("Finished");
}
fn parse_string_tuple(string: &str) -> Option<u32> {
if let Ok(i) = string.parse() {
Some(i)
} else {
None
}
}
This is what I've come up with in order to tackle this. It's just a stripped down version of what I want to do.
Basically I have two players. Player blue and player red. And both have their respective engines (which aren't necessarily the same in the real thing but for simplicity sake this should suffice).
Now from what I read in the tokio docs it would make sense to have a "manager" for the players similar to a manager and the different clients sending messages to it. In this case the manager has ownership over the player and manager_red should be able to tell manager when to stop the calculations for the main function to be able to continue.
Now with the above code the message from manager_red is transmitted to manager_blue and manager_blue "stops" and the main thread gets the message from manager_blue and manager_blue is calculating while manager_red is waiting on the human input.
I am aware that this code does not compile with the for loop in it (because the engines are moved in the first iteration of the loop and cannot be used in further iterations as such). Without it, it does what I expect it to do. However in my game the game is played with a loop for each turn or rather turn a player takes (alternating between the players). That might not be the best idea all by itself, but my question is now, what way there would be to mitigate this borrowing problem. Is there a way to "get back" the engines from the thread once they are finished or is there a completely different way I should go about doing this?
Edit: This is the compiler error message
error[E0382]: use of moved value: `player_blue`
--> src/main.rs:27:42
|
20 | let mut player_blue = Engine::new();
| --------------- move occurs because `player_blue` has type `Engine`, which does not implement the `Copy` trait
...
27 | let manager_blue = thread::spawn(move || {
| ^^^^^^^ value moved into closure here, in previous iteration of loop
...
31 | player_blue.make_move(t);
| ----------- use occurs due to use in closure
First, let's fix your compilation problems.
Using a variable in a thread that lives outside of the thread is only possible with scoped threads. Luckily, your situation is perfect for scoped threads.
Further, it is indeed necessary to use a move || {
closure at your manager_blue
thread because an mpsc::Receiver
is not shareable between threads. That said, you must prevent that the player_blue
itself gets moved into it. The easiest way to accomplish it to use a &mut
reference of player_blue
inside of the thread instead.
Here is the reworked code that now compiles:
use std::sync::mpsc;
use std::thread;
struct Engine {}
// Engine that plays the game. There are different ones in the actual game. This is for simplicity's sake
impl Engine {
pub fn new() -> Engine {
Engine {}
}
pub fn make_move(&mut self, gamestate: u32) {
println!("Player made move: {}", gamestate + 2);
}
}
fn main() {
let mut player_blue = Engine::new();
let mut player_red = Engine::new();
for _ in 0..2 {
let (tx_blue, rx_blue) = mpsc::channel();
let (tx_blue_back, rx_blue_back) = mpsc::channel();
// Prevent that this variable gets moved into the thread closure
let player_blue = &mut player_blue;
thread::scope(|s| {
s.spawn(move || {
let mut counter = 0;
loop {
if let Ok(t) = rx_blue.try_recv() {
player_blue.make_move(t);
tx_blue_back.send(counter).unwrap();
break;
}
counter += 1;
}
});
s.spawn(|| {
let mut input = String::new();
// Reading in input from human
println!("Please enter the important information we need:");
match std::io::stdin().read_line(&mut input) {
Ok(_) => (),
Err(error) => panic!("Problem reading in input: {:?}", error),
}
// Matching the input
let val = match parse_string_tuple(input.trim()) {
Some(i) => i,
None => 0,
};
// Make move
player_red.make_move(val);
// Tell other thread that reading in is finished and important calculations should be stopped
tx_blue.send(0).unwrap();
});
});
let counter = rx_blue_back.recv().unwrap();
println!("Counter is: {}", counter);
}
println!("Finished");
}
fn parse_string_tuple(string: &str) -> Option<u32> {
if let Ok(i) = string.parse() {
Some(i)
} else {
None
}
}
Alternatively, it is indeed possible to return values from threads back through the .join()
call:
use std::sync::mpsc;
use std::thread;
struct Engine {}
// Engine that plays the game. There are different ones in the actual game. This is for simplicity's sake
impl Engine {
pub fn new() -> Engine {
Engine {}
}
pub fn make_move(&mut self, gamestate: u32) {
println!("Player made move: {}", gamestate + 2);
}
}
fn main() {
let mut player_blue = Engine::new();
let mut player_red = Engine::new();
for _ in 0..2 {
let (tx_blue, rx_blue) = mpsc::channel();
let (tx_blue_back, rx_blue_back) = mpsc::channel();
let manager_blue = thread::spawn(move || {
let mut counter = 0;
loop {
if let Ok(t) = rx_blue.try_recv() {
player_blue.make_move(t);
tx_blue_back.send(counter).unwrap();
break;
}
counter += 1;
}
player_blue
});
let manager_red = thread::spawn(move || {
let mut input = String::new();
// Reading in input from human
println!("Please enter the important information we need:");
match std::io::stdin().read_line(&mut input) {
Ok(_) => (),
Err(error) => panic!("Problem reading in input: {:?}", error),
}
// Matching the input
let val = match parse_string_tuple(input.trim()) {
Some(i) => i,
None => 0,
};
// Make move
player_red.make_move(val);
// Tell other thread that reading in is finished and important calculations should be stopped
tx_blue.send(0).unwrap();
player_red
});
player_red = manager_red.join().unwrap();
player_blue = manager_blue.join().unwrap();
let counter = rx_blue_back.recv().unwrap();
println!("Counter is: {}", counter);
}
println!("Finished");
}
fn parse_string_tuple(string: &str) -> Option<u32> {
if let Ok(i) = string.parse() {
Some(i)
} else {
None
}
}
I'm not sure how to give you further guidance, however, because everything else from here on is opinion based. Yes, your approach is one possible way of solving this, as are many others. Which one is the correct one is both subjective and use case dependent; trying it out and seeing what issues you encounter is probably the right path.