I used to live in the single-thread JavaScript happy-land where the closest thing to working with threads I ever did was communicating between a website and a Chrome extension. So when people talked about the difficulties of parallelism and concurrency, I never truly got what the fuss was about.
As you may have read before, I started learning Rust a few weeks ago, re-writing a text-based game I previously made with Vue. It's a survival game in which you must gather and craft items to eat and drink. It has no winning condition other than trying to survive as many days as possible. I managed to get most of the game features working, but there was an annoying bug: if the user left the game idle for hours, it didn't check for the stats until the user interacted again. You could live for hundreds of days without doing nothing!
I knew this could be solved with threads, so I finally gathered the courage and read the chapter Fearless Concurrency of The Rust Programming Language.
Let's recap: what I needed was to be able to keep track of stats and days count every few seconds, and notify the player as soon as the stats reach 0. Spawning a new thread that runs some code every 10 seconds was easy:
thread::spawn(move || loop {
thread::sleep(Duration::from_secs(10));
println!("Now we should decrease stats and update day count…");
});
But how could I modify the stats and day count from that thread without running into ownership issues?
Turned out to be much easier than I was expecting. You can create a Mutex (mutual exclusion), so only one thread at a time can access that data. Multiple threads need to own that Mutex, so you need to wrap it in an Arc
(atomically reference counted) in order for the code to work properly (all this is much better explained in the Shared-State Concurrency chapter). The code ended up looking like this:
fn main() {
let stats = Arc::new(Mutex::new(Stats {
water: Stat::new(100.0),
food: Stat::new(100.0),
energy: Stat::new(100.0),
}));
let days = Arc::new(Mutex::new(0));
control_time(&days, &stats);
// ...
}
fn control_time(days: &Arc<Mutex<i32>>, stats: &Arc<Mutex<Stats>>) {
let now = Instant::now();
let days = Arc::clone(&days);
let stats = Arc::clone(&stats);
thread::spawn(move || loop {
thread::sleep(Duration::from_secs(10));
let elapsed_time = now.elapsed().as_secs();
let mut elapsed_days = days.lock().unwrap();
*elapsed_days = elapsed_time as i32 / 60;
let mut stats_lock = stats.lock().unwrap();
decrease_stats(&mut stats_lock, 10.0);
});
}
The main thread can keep using days
and stats
as before, just by adding the .lock()
.
And this works nice but hey, wait… I still have the same problem: the main thread is busy waiting for the user input! Even though stats and day count are updating successfully every 10 seconds, the main thread is not aware.
It was time to add another thread!
This thread is supposed to handle the user input, and send the action to the main one. For this one, it felt better to use the other way the Rust Programming Book explains to communicate between threads: message passing.
I needed a channel to send the action to the main thread, but the main thread also needed to let the input thread know when it was ready to receive an action (since some actions take time). The channels that Rust offers in the standard library are multiple producers, single consumer (meaning there is no two-way communication between channels), so I ended up creating two channels.
let (tx, rx) = mpsc::channel();
let (tx2, rx2) = mpsc::channel();
(not really creative with the names)
Then, spawn a new thread that will wait for the main thread signal, ask the user for input, and send it back to the main one. Note that using rx2.recv()
blocks the thread until a message is received: this will allow us to control when the user should be prompted.
thread::spawn(move || loop {
let _ = rx2.recv();
let action = request_input("\nWhat to do?");
tx.send(action).ok();
});
Then, from the main thread, we send a message to request input, and proceed to create a loop that will continuously check for stats and for user input with rx.try_recv()
(this doesn't block the thread). If the stats have reached 0, the loop will break, ending the game; if not, we ask for input again.
tx2.send(String::from("Ready for input")).ok();
loop {
if let Ok(action) = rx.try_recv() {
match action.trim() {
// handle all possible actions
}
}
if is_game_over(&stats.lock().unwrap()) {
break;
} else {
tx2.send(String::from("Ready for input")).ok();
}
}
It felt really natural to me: it's just like dispatching an event with JavaScript, right? Well, no.
When you dispatch an event with JavaScript, no one cares if someone is listening to that event. You dispatch it, and if there is no listener, that message is lost forever.
In Rust land, if a tree falls in a forest there has to be someone around to listen to the sound it makes. Otherwise, the forest panics and burns itself and the world explodes. And if the person who is around is busy doing other things, all the trees will wait in line and won't fall until that person stops to listen to them.
So what was happening was this:
As you see, it looks as if the input thread is not waiting until the ready message from the main one. However, the problem is that the main thread is sending messages continuously (remember that the action
is being listened to via try_recv
, so it's not blocking). Even though when the user inputs sleep
, the main thread indeed sleeps for a few seconds, since we had sent tons of messages before, the input thread gets the messages one by one. This might feel natural if you are used to other languages, but coming from JavaScript, it blew my mind and took me some time to wrap my mind around it.
In the end, the solution was as easy as sending the ready message only after the last message has been received and handled:
tx2.send(String::from("Ready for input")).ok();
loop {
if let Ok(action) = rx.try_recv() {
match action.trim() {
// handle all possible actions
}
// now we are ready for another action:
tx2.send(String::from("Ready for input")).ok();
}
if is_game_over(&stats.lock().unwrap()) {
break;
}
}
All problems solved!
As what I've been seeing these last few weeks, what's really difficult about Rust is not the language itself, but leaving behind the JavaScript way of thinking (feel free to replace JavaScript by your preferred language). However, that's what I like the most, getting out of the comfort zone!
You can check the game code here: https://github.com/codegram/live-rust
Cover photo by Geran de Klerk