#collection #access #cell #structures #lock #individual #shared

nightly lockerroom

Readers-writer access to individual cells of your collection!

3 unstable releases

0.2.1 Jul 25, 2024
0.2.0 Jul 19, 2024
0.1.0 Jul 16, 2024

#1836 in Data structures

MIT license

51KB
798 lines

Locker Room

Readers-writer access to individual cells of your collection!

LockerRoom and LockerRoomAsync

The central features of the crate is implemented by these structures. More specifically, they provide such functionality:

  1. Shared readers access to single cell of collection with LockerRoom::read_cell and LockerRoomAsync::read_cell;
  2. Exclusive writer access to single cell of collection with LockerRoom::write_cell and LockerRoomAsync::write_cell;
  3. Exclusive writer access to whole collection with LockerRoom::lock_room and LockerRoomAsync::lock_room.

But LockerRoomAsync is optional - you need to enable feature async to use it. It depends on tokio's RwLock.

LockerRoom example

let v = vec![0, 1, 2, 3, 4, 5];
let locker_room: LockerRoom<_> = v.into();
let locker_room = Arc::new(locker_room);
thread::scope(|scope| {
    scope.spawn(|| *locker_room.write_cell(0).unwrap() += 1);
    scope.spawn(|| *locker_room.write_cell(0).unwrap() += 2);
});
assert_eq!(3, *locker_room.read_cell(0).unwrap());

LockerRoomAsync example

let v = vec![0, 1, 2, 3, 4, 5];
let locker_room: LockerRoomAsync<_> = v.into();
let locker_room = Arc::new(locker_room);

let locker_room_cloned = Arc::clone(&locker_room);
let join1 = spawn(async move { *locker_room_cloned.write_cell(0).await.unwrap() += 1 });

let locker_room_cloned = Arc::clone(&locker_room);
let join2 = spawn(async move { *locker_room_cloned.write_cell(0).await.unwrap() += 2 });

join!(join1, join2);
assert_eq!(3, *locker_room.read_cell(0).await.unwrap());

Deadlock example

Carefully block multiple cells in one scope. Otherwise, situation like this may occur:

// Thread 1                            |  // Thread 2
let _w1 = locker_room.write_cell(0);   |
                                       |  let _w1 = locker_room.write_cell(1);
// will block
let _w2 = locker_room.write_cell(1);   |
                                       |  // will deadlock
                                       |  let _w2 = locker_room.write_cell(0);

Collections?

By default you can create LockerRoom and LockerRoomAsync from array, Vec, VecDeque, HashMap and BTreeMap.

But the crate provides traits, by which implementing to your collection, you can make it compatible with LockerRoom and LockerRoomAsync.

Collection

Crucial part of the crate that helps your collection to be compatible with LockerRoom and LockerRoomAsync.

Just implement it into your collection and everything will work!

Example

Let's implement the trait for the struct from Index's example:

enum Nucleotide {
    C,
    A,
    G,
    T,
}

struct NucleotideCount {
    pub a: usize,
    pub c: usize,
    pub g: usize,
    pub t: usize,
}

impl Collection for NucleotideCount {
    type Output = usize;
    type Idx = Nucleotide;
    type ShadowLocks = NucleotideShadowLocks;

    fn index(&self, index: impl Borrow<Self::Idx>) -> Option<&Self::Output> {
        Some(match index.borrow() {
            Nucleotide::A => &self.a,
            Nucleotide::C => &self.c,
            Nucleotide::G => &self.g,
            Nucleotide::T => &self.t,
        })
    }

    fn index_mut(&mut self, index: impl Borrow<Self::Idx>) -> Option<&mut Self::Output> {
        Some(match index.borrow() {
            Nucleotide::A => &mut self.a,
            Nucleotide::C => &mut self.c,
            Nucleotide::G => &mut self.g,
            Nucleotide::T => &mut self.t,
        })
    }

    fn indices(&self) -> impl Iterator<Item = Self::Idx> {
        [Nucleotide::A, Nucleotide::C, Nucleotide::G, Nucleotide::T].into_iter()
    }

    fn shadow_locks(&self) -> Self::ShadowLocks {
        Default::default()
    }
}

struct NucleotideShadowLocks {
    a: RwLock<()>,
    c: RwLock<()>,
    g: RwLock<()>,
    t: RwLock<()>,
}

impl ShadowLocksCollection for NucleotideShadowLocks {
    type Idx = Nucleotide;

    fn index(&self, index: impl Borrow<Self::Idx>) -> Option<&RwLock<()>> {
        Some(match index.borrow() {
            Nucleotide::A => &self.a,
            Nucleotide::C => &self.c,
            Nucleotide::G => &self.g,
            Nucleotide::T => &self.t,
        })
    }

    fn update_indices(&mut self, _indices: impl Iterator<Item = Self::Idx>) {
        // No need to reindex because NucleotideShadowLocks has static structure.
    }
}

If feature async is enabled, Collection must also include Collection::ShadowLocksAsync type and Collection::shadow_locks_async method. Thus ShadowLocksCollectionAsync must be implemented.

impl Collection for NucleotideCount {
    // ...
    type ShadowLocksAsync = NucleotideShadowLocksAsync;
    // ...
    fn shadow_locks_async(&self) -> Self::ShadowLocksAsync {
        Default::default()
    }
}

struct NucleotideShadowLocksAsync {
    a: tokio::sync::RwLock<()>,
    c: tokio::sync::RwLock<()>,
    g: tokio::sync::RwLock<()>,
    t: tokio::sync::RwLock<()>,
}

impl ShadowLocksCollectionAsync for NucleotideShadowLocksAsync {
    type Idx = Nucleotide;

    fn index(&self, index: impl Borrow<Self::Idx>) -> Option<&tokio::sync::RwLock<()>> {
        Some(match index.borrow() {
            Nucleotide::A => &self.a,
            Nucleotide::C => &self.c,
            Nucleotide::G => &self.g,
            Nucleotide::T => &self.t,
        })
    }

    fn update_indices(&mut self, _indices: impl Iterator<Item = Self::Idx>) {
        // No need to reindex because NucleotideShadowLocksAsync has static structure.
    }
}

Dependencies

~0–5.5MB
~19K SLoC