micah
micah

Reputation: 8096

Sharing Mutable Data Between Threads in Rust

I know there are hundreds of questions just like this one, but i'm having trouble wrapping my head around how to do the thing I'm trying to do.

I want an http server that accepts and processes events. On receiving/processing an event, i want the EventManager to send an update to an ApplicationMonitor that is tracking how many events have been accepted/processed. The ApplicationMonitor would also (eventually) handle things like tracking number of concurrent connections, but in this example I just want my EventManager to send an Inc('event_accepted') update to my ApplicationMonitor.

To be useful, I need the ApplicationMonitor to be able to return a snapshot of the stats when the requested through a /stats route.

enter image description here

So I have an ApplicationMonitor which spawns a thread and listens on a channel for incoming Stat events. When it receives a Stat event it updates the stats HashMap. The stats hashmap must be mutable within both ApplicationMonitor as well as the spawned thread.

use std::sync::mpsc;
use std::sync::mpsc::Sender;
use std::thread;
use std::thread::JoinHandle;
use std::collections::HashMap;

pub enum Stat {
    Inc(&'static str),
    Dec(&'static str),
    Set(&'static str, i32)
}

pub struct ApplicationMonitor {
    pub tx: Sender<Stat>,
    pub join_handle: JoinHandle<()>
}

impl ApplicationMonitor {
    pub fn new() -> ApplicationMonitor {
        let (tx, rx) = mpsc::channel::<Stat>();

        let mut stats: HashMap<&'static str, i32> = HashMap::new();

        let join_handle = thread::spawn(move || {
            for stat in rx.recv() {
                match stat {
                    Stat::Inc(nm) => {
                        let current_val = stats.entry(nm).or_insert(0);
                        stats.insert(nm, *current_val + 1);
                    },
                    Stat::Dec(nm) => {
                        let current_val = stats.entry(nm).or_insert(0);
                        stats.insert(nm, *current_val - 1);
                    },
                    Stat::Set(nm, val) => {
                        stats.insert(nm, val);
                    }
                }
            }
        });

        let am = ApplicationMonitor {
            tx,
            join_handle
        };

        am
    }

    pub fn get_snapshot(&self) -> HashMap<&'static str, i32> {
        self.stats.clone()
    }
}

Because rx cannot be cloned, I must move the references into the closure. When I do this, I am no longer able to access stats outside of the thread.

I thought maybe I needed a second channel so the thread could communicate it's internals back out, but this doesn't work as i would need another thread to listen for that in a non-blocking way.

Is this where I'd use Arc?

How can I have stats live inside and out of the thread context?

Upvotes: 0

Views: 2144

Answers (1)

Daniel Wagner-Hall
Daniel Wagner-Hall

Reputation: 2596

Yes, this is a place where you'd wrap your stats in an Arc so that you can have multiple references to it from different threads. But just wrapping in an Arc will only give you a read-only view of the HashMap - if you need to be able to modify it, you'll also need to wrap it in something which guarantees that only one thing can modify it at a time. So you'll probably end up with either an Arc<Mutex<HashMap<&'static str, i32>>> or a Arc<RwLock<HashMap<&'static str, i32>>>.

Alternatively, if you're just changing the values, and not adding or removing values, you could potentially use an Arc<HashMap<&static str, AtomicU32>>, which would allow you to read and modify different values in parallel without needing to take out a Map-wide lock, but Atomics can be a little more fiddly to understand and use correctly than locks.

Upvotes: 3

Related Questions