Contents

Thread Macros in Rust

In my current WIP in Rust I needed to spawn a number of different threads, then wait for them to complete.

Niave Thread Spawn and Join

My first attempt was pretty much what the docs say to do:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
let mut threads = vec![];
threads.push(thread::spawn(move || {
    reader::run(sync_sets.clone(), cli, tx_reader_to_core)
}));
threads.push(thread::spawn(move || {
    writer::run(sync_sets, rx_core_to_writer, tx_writer_to_core)
}));
threads.push(thread::spawn(move || {
    store::run(store, rx_core_to_store, tx_store_to_core)
}));
threads.push(thread::spawn(move || {
    core::run(
        rx_reader_to_core,
        rx_store_to_core,
        rx_writer_to_core,
        tx_core_to_store,
        tx_core_to_writer,
    )
}));

for thread in threads {
    thread.join().unwrap_or(());
}

There’s a fair bit of repetition there for each thread, and it’s hiding the important details in the middle of it all.

Repetition like this is where Rust’s declaritive macros can help.

Using Macros

I created two macros, one to handle the spawning and another to wrap that and join all the threads.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
macro_rules! spawn {
    ($($x:expr),*) => {
        {
            let mut threads = vec![];
            $(threads.push(std::thread::spawn(move || $x));)*
            threads
        }
    };
}

macro_rules! spawn_and_join {
    ($($x:expr),*) => {
        let threads = spawn!($($x),*);
        for thread in threads {
            thread.join().unwrap_or(());
        }
    };
}

This allows the code we saw earlier to be condensed to just:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
spawn_and_join!(
    writer::run(writer_syncfile, rx_core_to_writer, tx_writer_to_core),
    store::run(store, rx_core_to_store, tx_store_to_core),
    core::run(
        rx_reader_to_core,
        rx_store_to_core,
        rx_writer_to_core,
        tx_core_to_store,
        tx_core_to_writer,
    ),
    reader::run(reader_syncfile, cli, tx_reader_to_core)
);

I then saw that a todo!() in my writer thread was causing the thread to panic, and the log message was that a thread unnamed had paniced. There was more details that pointed to what I need to do next in this project, but I thought I should name the threads before moving on to that.

Named Threads

I didn’t want to supply the thread names as a String or &str. I want to keep the noise in the code down to a minimum. So I use an ident and the stringify! macro to let me do the following:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
spawn_and_join!(
   writer: writer::run(writer_syncfile, rx_core_to_writer, tx_writer_to_core),
   store: store::run(store, rx_core_to_store, tx_store_to_core),
  core: core::run(
       rx_reader_to_core,
       rx_store_to_core,
       rx_writer_to_core,
       tx_core_to_store,
       tx_core_to_writer,
    ),
    reader: reader::run(reader_syncfile, cli, tx_reader_to_core)
);

Each thread name is an ident prefixing the expr that is the body of the new thread.

To specify the name of the thread, we can’t just use std::thread::spawn, we need to use std::thread::Builder::new().name(...).

The updated macros are here:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
macro_rules! spawn {
    ($($name:ident: $x:expr),*) => {
        {
            let mut threads = vec![];
            $(threads.push({
                let thread = std::thread::Builder::new().name(stringify!($name).to_owned());
                thread.spawn(move || $x)?
            });)*
            threads
        }
    };
}

macro_rules! spawn_and_join {
    ($($name:ident: $x:expr),*) => {
        let threads = spawn!($($name: $x),*);
        for thread in threads {
            thread.join().unwrap_or(());
        }
    };
}