Crate uni_rc_lock

source ·
Expand description

A trait for universal reference-counted lock with interior mutability allowing multiple readers or a single writer, which may represent either Rc<RefCell<T>> or Arc<RwLock<T>>.

Basics

Motivation

The Rc<RefCell<T>> and Arc<RwLock<T>> are idiomatic ways of expressing interior mutability in Rust in the single-threaded and multi-threaded scenarious respectively. Both types provide essentially the same interface with multiple readers or a single writer for their managed data.

However, despite their similarity, these types do not share any common trait and thus can’t be used in a generic way: one can’t define a function argument or a struct field that could accept either Rc<RefCell<T>> or Arc<RwLock<T>>. This leads to code duplication if it is not known in advance which kind of smart pointer (Rc or Arc) will be passed.

UniRcLock solves this problem by providing a common trait for Rc<RefCell<T>> and Arc<RwLock<T>> so that they could be used in a generic way in single-threaded and multi-threaded scenarious alike.

Performance

UniRcLock is a zero-cost abstraction.

Limitations

An ability to recover from lock poisoning in RwLock<T> is lost when using UniRcLock. The methods read() and write() will panic if the lock is poisoned.

Examples

A generic function which accepts both Rc<RefCell<T>> and Arc<RwLock<T>>:

#[derive(Debug)]
struct Foo(i32);

fn incr_foo(v: impl UniRcLock<Foo>){
    v.write().0 += 1;
}

// Using Rc
let ptr1 = Rc::new(RefCell::new(Foo(0)));
incr_foo(ptr1.clone());
println!("After increment: {:?}", ptr1);

// Using Arc
let ptr2 = Arc::new(RwLock::new(Foo(0)));
incr_foo(ptr2.clone());
println!("After increment: {:?}", ptr2);

Example of generic struct, which can hold either Rc<RefCell<T>> or Arc<RwLock<T>>:

// A user struct
struct State {val: i32}

// Generic wrapper
#[derive(Debug,Clone)]
struct StateHandler<T: UniRcLock<State>> {
    state: T,
}

impl<T: UniRcLock<State>> StateHandler<T> {
    // Constructor taking either Rc<RefCell<T>>` or `Arc<RwLock<T>>
    fn new(val: T) -> Self {
        Self{state: val}
    }
}

// Using with Rc
{
    let st = Rc::new(RefCell::new(State { val: 42 }));
    let st_handler = StateHandler::new(st);
    st_handler.state.write().val += 1;
    println!("{}", st_handler.state.read().val);
}

// Using with Arc in exactly the same way
{
    let st = Arc::new(RwLock::new(State { val: 42 }));
    let st_handler = StateHandler::new(st);
    st_handler.state.write().val += 1;
    println!("{}", st_handler.state.read().val);
}

// Using in multiple threads with Arc
{
    let st = Arc::new(RwLock::new(State { val: 42 }));

    let st_handler = StateHandler::new(st);
    let threads: Vec<_> = (0..10)
        .map(|i| {
            let h = st_handler.clone();
            thread::spawn(move || {
                h.state.write().val += 1;
                println!("Thread #{i} incremented");
            })
        })
        .collect();

    for t in threads {
        t.join().unwrap();
    }
 
    println!("Result: {}", st_handler.state.read().val);
}

Expectibly, this example won’t compile with Rc since it doesn’t implement Send.

Traits

  • A common trait for Rc<RefCell<T>> and Arc<RwLock<T>>