Search code examples
rustclosures

Method for maintaining access to a variable that is moved into a closure


I am trying to move values into a ctrlc closure, but update these same values in the main block until the Ctrl+C signal is seen. At which point the closure will call a function that displays some stats about the values.

Specifically, I am recreating ping. I have variables requests and replies that track the number of each that occurs during the program's life. When the user input Ctrl+C, the program will display the total requests, replies, and the percentage of packets lost.

Example:

fn ping(ip: IpAddr, frequency: u32) -> () {
    let requests = 0;
    let replies = 0;
    ctrlc::set_handler(move || calc_stats(requests, replies)).expect("");

    //each echo request increments request by 1
    //each echo reply increments replies by 1

    //eventually user enter Ctrl+C, the closure is called and in turn calc_stats is called

Now I know the code above will not work because requests and replies would be moved. But I tried using a Box and even Arc but run into an assortment of errors. The Box approach works when I clone:

let closure = {
        let crequests = requests.clone();
        let creplies = replies.clone();
        ctrlc::set_handler(move || calc_stats(crequests.clone(), creplies.clone(), &min, &max, &rtts))
        .expect("Could not calculate stats");
    };

But the requests and replies variables are still their initial values (0).

What is the ideal approach in this scenario?


Solution

  • Arc is the correct move. However, it doesn't allow you to get an &mut to the contents, so you need an interior mutability construct, like Mutex.

    pub fn ping(ip: IpAddr, frequency: u32) {
        let requests = Arc::new(Mutex::new(0));
        let replies = Arc::new(Mutex::new(0));
    
        let state_clone = state.clone();
        ctrlc::set_handler(move || calc_stats(&requests, &replies)).unwrap();
    }
    
    fn calc_stats(requests: &Mutex<i32>, replies: &Mutex<i32>) {
        todo!()
    }
    

    It is a bit inefficient to have two mutexes here, so you could put all your state variables into a struct.

    struct State {
        requests: u32,
        replies: u32,
    }
    
    impl State {
        fn new() -> Self {
            State {
                requests: 0,
                replies: 0,
            }
        }
    }
    

    And then wrap that State in a mutex. Here's a whole example, minus the actual pinging.

    pub fn ping(ip: IpAddr, frequency: u32) {
        let state = Arc::new(Mutex::new(State::new()));
    
        let state_clone = state.clone();
        ctrlc::set_handler(move || calc_stats(&state_clone)).unwrap();
    
        do_things(&state, ip, frequency);
    }
    
    fn calc_stats(state: &Mutex<State>) {
        let state = state.lock().unwrap();
        println!("Requests: {}\nReplies: {}", state.requests, state.replies);
        std::process::exit(0);
    }
    
    fn do_things(state: &Mutex<State>, ip: IpAddr, frequency: u32) {
        loop {
            {
                println!("Request");
                let mut state = state.lock().unwrap();
                state.requests += 1;
            }
            std::thread::sleep(std::time::Duration::from_secs(1));
            {
                println!("Reply");
                let mut state = state.lock().unwrap();
                state.replies += 1;
            }
        }
    }
    

    Just ensure you leave the mutex unlocked at some point so that the ctrlc handler can read it.

    A more performant implementation would use the Atomic* integers instead of Mutex, but Mutex is more general and easier to use.

    Also some general advice, more errors does not necessarily mean your code is more incorrect.