Efficient way to pass read-only shared "state" to tokio::spawn?

Hi guys, I am coming from a strong Node.JS async background (async/await + Promises), and trying to learn async Rust with Tokio.

I am able to get stuff to "work", but as part of coming to Rust I want to try and be as efficient as possible. My program needs to load up a config at startup (which can be considered "read-only" for the rest of the lifetime of the program). This then needs to be passed to handlers (which are tokio::spawned) for some use.

A very basic example program could be:

use tokio::{io::AsyncWriteExt, net::TcpListener};

#[derive(Clone)]
struct Config {
    data: String,
}

#[tokio::main]
async fn main() {
    let listener = TcpListener::bind("127.0.0.1:6379").await.unwrap();

    // Assume config is initialized / read when program starts
    // e.g. from CLI argument or config file
    let cfg = Config {
        data: "HELLO".to_string()
    };

    loop {
        let (mut socket, _) = listener.accept().await.unwrap();
        let cfg_copy = cfg.clone();
        tokio::spawn(async move {
            // assume we need to spawn for some long running I/O task
            // but we need to use config (for READ ONLY) in this
            socket.write(cfg_copy.data.as_bytes()).await;
        });
    }
}

I wanted to know, is there a better way to pass Config down to the guys who need it? I am guessing using .clone() comes with some extra memory use?

If I wrap it in an Arc and clone that, does it make any difference? From what I can tell, I must use async move to pass the socket inside.

Thanks for your help.

Arc would be less memory use than directly cloning (all cloned Arcs would share the same Config).

You can use something like OnceLock or (in 1.80+ i.e. the near future) LazyLock. Or even just leak it.[1]


  1. Since your Config has no lifetimes, you can get a &'static Config from leaking. ↩︎

Thanks for the suggestion. In my use case, since I know that the config must exist at the program start, and then it must be used (i.e. be "available") for the rest of the program, Box::leak seems like the simplest solution, since it guarantees that it will be readable from the ::spawn, and only use memory once. Very useful!

OnceLock and LazyLock look interesting, but I wonder if they are overkill in this scenario, where I know the initialization must happen in main before doing anything else (i.e. I don't need thread-safe initialization).

Also thanks for informing me on the Arc cloning, will definitely keep that in mind if I ever need something which can't be leaked out, since it seems cheaper than cloning the whole object (which could get quite big).

For reference to anyone searching, this is how I leaked the config:

#[tokio::main]
async fn main() {
    let listener = TcpListener::bind("127.0.0.1:6379").await.unwrap();

    // Assume config is initialized / read when program starts
    // e.g. from CLI argument or config file
    let cfg = Box::new(Config {
        data: "HELLO".to_string()
    });
    let leaked: &'static Config = Box::leak(cfg);

    loop {
        let (mut socket, _) = listener.accept().await.unwrap();
        tokio::spawn(async move {
            // assume we need to spawn for some long running I/O task
            // but we need to use config (for READ ONLY) in this
            socket.write(&leaked.data.as_bytes()).await;
        });
    }
}
1 Like