Reddit Writes Rust

3 minute read Published:

Read about how the Rust subreddit helped me implement multi-threading in my command-line music player

I wrote a command-line YouTube-backend music player in Rust, called surge (SOURCE).

The reddit post is here.

The key components are:

  • YouTube API to find songs
  • youtube-dl to download them
  • libmpv to play them

The motivation behind surge is twofold - I’m trying to minimize my use of the mouse, and I wanted readline features in a music player, e.g. ctrl-r to reverse search an album I had searched before.

What I wanted to achieve

As described in the linked Reddit post, my application was designed like this:

loop {
    // receive user events - play song, queue song, search song
    line => cmd.execute(line),
}

cmd is a monolithic struct which contains the libmpv audio player and the actual session information - latest search results, now playing, etc. cmd knows how to understand and execute commands.

My desire was to implement a continuous-playback feature. Naively, this would be a thread which behaved like a human constantly queueing related songs.

This seemed easy at first because I had the building blocks:

  • I had the related command to load related songs
  • I had the queue command to queue songs from the loaded list

To put it very simply, I needed a thread which could send ["related", "queue"] to cmd in a loop.

However, I had no idea where to put/spawn this robot thread within the readline loop.

Enter the mpsc

The mpsc is:

Multi-producer, single-consumer FIFO queue communication primitives. This module provides message-based communication over channels

The key suggestion I received from Reddit was to separate my application logic into the following:

  • A command thread to receive commands
  • A readline thread to take human input and send commands to the command thread
  • A robot thread to send the automated continuous playback commands to the command thread
let (tx, rx) = channel();
let tx_ = tx.clone();

let mut threads = Vec::with_capacity(3);

threads.push(command_thread(..., &rx, ...))
threads.push(readline_thread(..., &tx, ...))
threads.push(robot_thread(..., &tx_, ...))

for t in threads {
    if let Ok(t) = t {
        t.join().expect("Couldn't join thread");
    }
}

Note that the Sender is clonable to send it to other threads.

AtomicBool to control the robot thread

Another design choice arose when I had to find a way for the human readline thread to control the robot thread.

At first I was ready to use the mpsc again - a new set of senders and receivers. However, since this is a hobby project for learning, I wanted to try a different solution.

Enter the AtomicBool.

N.B. I didn’t magically know about the AtomicBool - I started with a Mutex<bool> and clippy told me to use an AtomicBool instead. I can’t overstate how helpful clippy is to learn idiomatic Rust.

An AtomicBool is a thread-safe boolean:

//readline thread toggling the ROBOT_SHOULD_RUN AtomicBool 
ROBOT_SHOULD_RUN.store(
    !ROBOT_SHOULD_RUN.load(Ordering::SeqCst),
    Ordering::SeqCst,
);

//robot thread checking if it should run
if !ROBOT_SHOULD_RUN.load(Ordering::SeqCst)  {
    thread::sleep(time::Duration::from_secs(1));
}

SeqCst?

I don’t know either. Somebody at work told me “just use SeqCst - sequential consistency - and once you’re comfortable with that, I’ll explain the other options to you”.

The Ordering doc explains the various consistency options. This is kinda scary stuff for me to read so for now I haven’t dug too deep.

Conclusion

Ask for help. The Rust subreddit community is helpful, and when you’re stuck bashing your head against something, it’s worth it to take a step back and ask for the opinions of others.