Search code examples
asynchronousrustmemoryrust-tokio

How do I use this mutable reference of a struct in a loop spawning async tasks without having to use the Copy/Clone traits?


I'm currently building a simple in-memory database to get back into 'learning' rust, but for the life of me I cannot seem to figure out a work around for what I'm trying to do.

I'm using tokio for asynchronous io/networking.

Here's the code:

let mut cache = Cache {
    buffer: vec![0; allocated],
    mapping: HashMap::new()
};

let listener = TcpListener::bind("0.0.0.0:9055").await?;

loop {
    let (mut socket, _) = listener.accept().await?;

    tokio::spawn(async move {
        process_client(&mut cache, &mut socket).await?;
        Ok::<_, io::Error>(())
    });
}

Definition of the Cache struct:

struct Cache {
    buffer: Vec<u8>,
    mapping: HashMap<String, (usize, usize)>
}

Now the error I keep getting is this:

error[E0382]: use of moved value: `cache`
   --> src\main.rs:172:22
    |
162 |       let mut cache = Cache {
    |           --------- move occurs because `cache` has type `Cache`, which does not implement the `Copy` trait
...
172 |           tokio::spawn(async move {
    |  ______________________^
173 | |             process_client(&mut cache, &mut socket).await?;
    | |                                 ----- use occurs due to use in generator
174 | |             Ok::<_, io::Error>(())
175 | |         });
    | |_________^ value moved here, in previous iteration of loop

How can I pass a mutable reference of cache to the process_client() method in this async task without having to clone/copy the entire cache struct? (since cloning/copying would be a huge performance hit if the buffer was large)

I've tried to implement lifetimes, but I do not have a good understanding of lifetimes and how to use them properly.


Solution

  • Without some form of synchronisation this cannot be done. This is by design, the compiler is preventing you from a potential race condition. Consider the case where tokio is running on multiple threads, the spawn gets hit and deferred to a different thread, the loop then repeats and the second spawn is hit and deferred to a different thread. You now have two mutable references to cache on two different threads, with potentially concurrent accesses.

    To solve this you can use something like Mutex, in this case you should use tokio::sync::Mutex (or RwLock if you ever only need immutable borrows), along with an Arc (as otherwise there is no way to ensure that cache will live long enough and allow multiple valid references to it). For example:

    let mut cache = Arc::new(Mutex::new(Cache {
        buffer: vec![0; allocated],
        mapping: HashMap::new()
    }));
    // ...
    tokio::spawn({
        let cache = cache.clone();
        async move {
            process_client(&mut cache.lock().await, &mut socket).await?;
            // ...
        }
    });