What is the best implementation for modifying a shared variable for different closures

Hi all,

I am thinking about modifying a shared variable by two different closures. The pseudocode is like this:

def counter(x: Int) = {
    val c = new Ref(x) // new a reference referring to a integer 5
    (() => c += 1, () => c -= 1)
} 
val (increase, decrease) = counter(5)
increase()
decrease() // result: 5

According to kornel's suggestions in Heap-allocated value with associated lifetime, I have implemented the code in two versions:

Rc:
https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=8f4b69cbf7b681f78cf087a9a5759274

use std::boxed::Box;
use std::cell::Cell;
use std::ops::Fn;
use std::rc::Rc;

fn main() {
    fn counter(x: i16) -> (Box<dyn Fn() -> i16>, Box<dyn Fn() -> i16>) {
        let v: Rc<Cell<i16>> = Rc::new(Cell::new(x));
        let b1 = v.clone();
        let b2 = v.clone();
        (
            Box::new({
                move || {
                    b1.set(b1.get() + 1);
                    return b1.get();
                }
            }),
            Box::new({
                move || {
                    b2.set(b2.get() - 1);
                    return b2.get();
                }
            })
        )
    }

    let (inc, dec) = counter(5);

    let res = inc();
    println!("{}", res);
    let res = inc();
    println!("{}", res);
    let res = dec();
    println!("{}", res);
}

Arc:
https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=97920c7bc324d6e9f3d5e249fe616d0c

use core::sync::atomic::AtomicI16;
use std::boxed::Box;
use std::cell::Cell;
use std::ops::Fn;
use std::rc::Rc;
use std::sync::Arc;
use std::sync::atomic::Ordering;

fn main() {
    fn counter(x: i16) -> (Box<dyn Fn() -> i16>, Box<dyn Fn() -> i16>) {
        let v: Arc<AtomicI16> = Arc::new(AtomicI16::new(x));
        let b1 = v.clone();
        let b2 = v.clone();
        (
            Box::new({
                move || {
                    b1.store(b1.load(Ordering::SeqCst) + 1, Ordering::SeqCst);
                    return b1.load(Ordering::SeqCst);
                }
            }),
            Box::new({
                move || {
                    b2.store(b2.load(Ordering::SeqCst) - 1, Ordering::SeqCst);
                    return b2.load(Ordering::SeqCst);
                }
            })
        )
    }
    
    // Ordering::Relaxed sync in one thread
    // Ordering::SeqCst sync among all the threads

    let (inc, dec) = counter(5);

    let res = inc();
    println!("{}", res);
    let res = inc();
    println!("{}", res);
    // drop(state); // uncomment for borrow error
    let res = dec();
    println!("{}", res);
}

I am wondering which one is a better implementation for my pseudocode? If neither is, could someone tell me the best implementation? Thanks!

Arc<Atomic…> is the best type for an integer shared between closures.

You should not use load + store, because that allows race conditions. Use fetch_add or compare_exchange instead.

Alternatively, share Arc<Mutex<YourStructType>> where you can store multiple values and non-integer values.

2 Likes

Using Arc and atomics only makes sense if you're going to use the closures on a different thread than where you created them. This is - in your case - prohibited anyways by the lack of Send and/or Sync bounds on your Box<dyn Fn…> trait objects. In other words, if you take the approach of supporting multi-threaded use, then you'll need to write Box<dyn Fn() -> i16 + Send + Sync> instead.

Another option is to, instead of boxing, use an impl Trait return type. counter(x: i16) -> (impl Fn() -> i16, impl Fn() -> i16). IIRC, that one would implicitly add the appropriate Send/Sync bounds, so you wouldn't need to add those yourself.

3 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.