Skip to content

Commit

Permalink
perf: use hashbrown::RawTable + dashmap's shard selection strategy
Browse files Browse the repository at this point in the history
docs: document API types and methods, add doc tests
  • Loading branch information
willothy committed Nov 3, 2024
1 parent f9ad383 commit c47f3ef
Show file tree
Hide file tree
Showing 9 changed files with 430 additions and 195 deletions.
29 changes: 4 additions & 25 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ categories = ["asynchronous", "concurrency", "data-structures"]

[dependencies]
crossbeam-utils = "0.8.20"
hashbrown = { version = "0.15" }
hashbrown = { version = "0.14.5", default-features = false, features = ["raw"] }
lock_api = "0.4.12"
parking_lot_core = "0.9.10"

[dev-dependencies]
tokio = { version = "1.41.0", features = ["full"] }
50 changes: 48 additions & 2 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,62 @@
//! # Whirlwind
//!
//! A collection of data structures that allow for concurrent access to shared data.
//!
//! Currently, this crate provides the following data structures:
//!
//! - [`ShardMap`]: A concurrent hashmap using a sharding strategy.
//! - [`ShardSet`]: A concurrent set based on a [`ShardMap`] with values of `()`.
//!
//! ## ShardMap
//!
//! A concurrent hashmap using a sharding strategy.
//!
//! ### Example
//!
//! ```rust
//! use tokio::runtime::Runtime;
//! use std::sync::Arc;
//! use whirlwind::ShardMap;
//!
//! let rt = Runtime::new().unwrap();
//! let map = Arc::new(ShardMap::new());
//!
//! rt.block_on(async {
//! map.insert("foo", "bar").await;
//! assert_eq!(map.len().await, 1);
//! assert_eq!(map.contains_key(&"foo").await, true);
//! });
//! ```
//!
//! ## ShardSet
//!
//! A concurrent set based on a [`ShardMap`] with values of `()`.
//!
//! ### Example
//!
//! ```rust
//! use tokio::runtime::Runtime;
//! use std::sync::Arc;
//! use whirlwind::ShardSet;
//!
//! let rt = Runtime::new().unwrap();
//! let set = Arc::new(ShardSet::new());
//! rt.block_on(async {
//! set.insert("foo").await;
//! assert_eq!(set.contains(&"foo").await, true);
//! set.remove(&"foo").await;
//! assert_eq!(set.contains(&"foo").await, false);
//! assert_eq!(set.len().await, 0);
//! });
//! ```
//!
//!
//! See the documentation for each data structure for more information.

pub mod mapref;
mod shard;
pub mod shard_map;
pub mod shard_set;
mod shard_map;
mod shard_set;

pub use shard_map::ShardMap;
pub use shard_set::ShardSet;
20 changes: 16 additions & 4 deletions src/mapref.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,26 @@
//! let map = Arc::new(ShardMap::new());
//! rt.block_on(async {
//! map.insert("foo", "bar").await;
//!
//! let r = map.get(&"foo").await.unwrap();
//!
//! assert_eq!(r.key(), &"foo");
//! assert_eq!(r.value(), &"bar");
//!
//! drop(r); // release the lock so we can mutate the value.
//!
//! let mut mr = map.get_mut(&"foo").await.unwrap();
//! *mr.value_mut() = "baz";
//!
//! assert_eq!(mr.value(), &"baz");
//! });

use crate::shard::{ShardReader, ShardWriter};

/// A reference to a key-value pair in a [`crate::ShardMap`]. Holds a shared (read-only) lock on the shard
/// associated with the key.
/// A reference to a key-value pair in a [`crate::ShardMap`].
///
/// Holds a shared (read-only) lock on the shard associated with the key. Dropping this
/// reference will release the lock.
pub struct MapRef<'a, K, V> {
key: &'a K,
value: &'a V,
Expand All @@ -51,21 +58,26 @@ where
Self { reader, key, value }
}

/// Returns a reference to the key.
pub fn key(&self) -> &K {
self.key
}

/// Returns a reference to the value.
pub fn value(&self) -> &V {
self.value
}

/// Returns a reference to the key-value pair
pub fn pair(&self) -> (&K, &V) {
(self.key, self.value)
}
}

/// A mutable reference to a key-value pair in a [`crate::ShardMap`]. Holds an exclusive lock on
/// the shard associated with the key.
/// A mutable reference to a key-value pair in a [`crate::ShardMap`].
///
/// Holds an exclusive lock on the shard associated with the key. Dropping this
/// reference will release the lock.
pub struct MapRefMut<'a, K, V> {
key: &'a K,
value: &'a mut V,
Expand Down
68 changes: 11 additions & 57 deletions src/shard.rs
Original file line number Diff line number Diff line change
@@ -1,85 +1,39 @@
use hashbrown::HashTable;

use std::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard};

use std::{
future::Future,
pin::Pin,
task::{Context, Poll},
};
mod futures;

use futures::{Read, Write};

pub(crate) type ShardInner<K, V> = HashTable<(K, V)>;
pub(crate) type ShardReader<'a, K, V> = RwLockReadGuard<'a, ShardInner<K, V>>;
pub(crate) type ShardWriter<'a, K, V> = RwLockWriteGuard<'a, ShardInner<K, V>>;
pub(crate) type Inner<K, V> = hashbrown::raw::RawTable<(K, V)>;
pub(crate) type ShardReader<'a, K, V> = RwLockReadGuard<'a, Inner<K, V>>;
pub(crate) type ShardWriter<'a, K, V> = RwLockWriteGuard<'a, Inner<K, V>>;

/// A shard in a [`crate::ShardMap`]. Each shard contains a [`hashbrown::HashTable`] of key-value pairs.
pub(crate) struct Shard<K, V> {
data: RwLock<ShardInner<K, V>>,
}

pub struct Read<'a, K, V> {
shard: &'a Shard<K, V>,
}

impl<'a, K, V> Future for Read<'a, K, V> {
type Output = ShardReader<'a, K, V>;

fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output> {
match self.shard.data.try_read() {
Ok(guard) => Poll::Ready(guard),
_ => {
cx.waker().wake_by_ref();
Poll::Pending
}
}
}
}

pub struct Write<'a, K, V> {
shard: &'a Shard<K, V>,
}

impl<'a, K, V> Future for Write<'a, K, V> {
type Output = ShardWriter<'a, K, V>;

fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output> {
match self.shard.data.try_write() {
Ok(guard) => Poll::Ready(guard),
_ => {
cx.waker().wake_by_ref();
Poll::Pending
}
}
}
data: RwLock<Inner<K, V>>,
}

impl<K, V> Shard<K, V>
where
K: Eq + std::hash::Hash,
{
pub fn new() -> Self {
Self {
data: RwLock::new(ShardInner::new()),
}
}

pub fn with_capacity(capacity: usize) -> Self {
Self {
data: RwLock::new(ShardInner::with_capacity(capacity)),
data: RwLock::new(Inner::with_capacity(capacity)),
}
}

pub async fn write<'a>(&'a self) -> ShardWriter<'a, K, V> {
Write { shard: self }.await
Write::new(self).await
}

pub async fn read<'a>(&'a self) -> ShardReader<'a, K, V> {
Read { shard: self }.await
Read::new(self).await
}
}

impl<K, V> std::ops::Deref for Shard<K, V> {
type Target = RwLock<ShardInner<K, V>>;
type Target = RwLock<Inner<K, V>>;

fn deref(&self) -> &Self::Target {
&self.data
Expand Down
56 changes: 56 additions & 0 deletions src/shard/futures.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
use std::{
future::Future,
pin::Pin,
task::{Context, Poll},
};

use super::{Shard, ShardReader, ShardWriter};

/// A future that resolves to a read lock on a shard.
pub(crate) struct Read<'a, K, V> {
shard: &'a Shard<K, V>,
}

impl<'a, K, V> Read<'a, K, V> {
pub(crate) fn new(shard: &'a Shard<K, V>) -> Self {
Self { shard }
}
}

impl<'a, K, V> Future for Read<'a, K, V> {
type Output = ShardReader<'a, K, V>;

fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output> {
match self.shard.data.try_read() {
Ok(guard) => Poll::Ready(guard),
_ => {
cx.waker().wake_by_ref();
Poll::Pending
}
}
}
}

pub(crate) struct Write<'a, K, V> {
shard: &'a Shard<K, V>,
}

impl<'a, K, V> Write<'a, K, V> {
pub(crate) fn new(shard: &'a Shard<K, V>) -> Self {
Self { shard }
}
}

impl<'a, K, V> Future for Write<'a, K, V> {
type Output = ShardWriter<'a, K, V>;

fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output> {
match self.shard.data.try_write() {
Ok(guard) => Poll::Ready(guard),
_ => {
cx.waker().wake_by_ref();
Poll::Pending
}
}
}
}
Loading

0 comments on commit c47f3ef

Please sign in to comment.