Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "lockmap"
version = "0.1.15"
version = "0.1.16"
edition = "2021"

authors = ["SF-Zhou <sfzhou.scut@gmail.com>"]
Expand Down
52 changes: 44 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,37 +6,73 @@
[![Documentation](https://docs.rs/lockmap/badge.svg)](https://docs.rs/lockmap)
[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2FSF-Zhou%2Flockmap.svg?type=shield)](https://app.fossa.com/projects/git%2Bgithub.com%2FSF-Zhou%2Flockmap?ref=badge_shield)

A high-performance, thread-safe HashMap implementation for Rust that provides fine-grained locking at the key level.
**LockMap** is a high-performance, thread-safe HashMap implementation for Rust that provides **fine-grained locking at the key level**.

Unlike standard concurrent maps that might lock the entire map or large buckets, `LockMap` allows you to hold an exclusive lock on a specific key (including non-existent ones) for complex atomic operations, minimizing contention across different keys.

## Features

* **Key-Level Locking**: Acquire exclusive locks for specific keys. Operations on different keys run in parallel.
* **Sharding Architecture**: Internal sharding reduces contention on the map structure itself during insertions and removals.
* **Deadlock Prevention**: Provides `batch_lock` to safely acquire locks on multiple keys simultaneously using a deterministic order.
* **Efficient Waiting**: Uses a hybrid spin-then-park Futex implementation for low-overhead locking.
* **Entry API**: Ergonomic RAII guards (`EntryByVal`, `EntryByRef`) for managing locks.

## Important Caveats

### 1. No Lock Poisoning

Unlike `std::sync::Mutex`, **this library does not implement lock poisoning**. If a thread panics while holding an `Entry`, the lock is released immediately (via Drop) to avoid deadlocks, but the data is **not** marked as poisoned.
> **Warning**: Users must ensure exception safety. If a panic occurs during a partial update, the data associated with that key may be left in an inconsistent state for subsequent readers.

### 2. `get()` Performance

The `map.get(key)` method clones the value while holding an internal shard lock.
> **Note**: If your value type `V` is expensive to clone (e.g., deep copy of large structures), or if `clone()` acquires other locks, use `map.entry(key).get()` instead. This moves the clone operation outside the internal map lock, preventing blocking of other threads accessing the same shard.

## Usage

```rust
use lockmap::LockMap;
use std::collections::BTreeSet;

// Create a new lock map
let map = LockMap::<String, String>::new();

// Set a value
// 1. Basic Insert
map.insert_by_ref("key", "value".into());

// Get a value
// 2. Get a value (Clones the value)
assert_eq!(map.get("key"), Some("value".into()));

// Use entry API for exclusive access
// 3. Entry API: Exclusive access (Read/Write)
// This locks ONLY "key", other threads can access "other_key" concurrently.
{
let mut entry = map.entry_by_ref("key");

// Check value
assert_eq!(entry.get().as_deref(), Some("value"));

// Update value atomically
entry.insert("new value".to_string());
}
} // Lock is automatically released here

// Remove a value
// 4. Remove a value
assert_eq!(map.remove("key"), Some("new value".into()));

// Batch lock.
let mut keys = std::collections::BTreeSet::new();
// 5. Batch Locking (Deadlock safe)
// Acquires locks for multiple keys in a deterministic order.
let mut keys = BTreeSet::new();
keys.insert("key1".to_string());
keys.insert("key2".to_string());

// `locked_entries` holds all the locks
let mut locked_entries = map.batch_lock::<std::collections::HashMap<_, _>>(keys);

if let Some(mut entry) = locked_entries.get_mut("key1") {
entry.insert("updated_in_batch".into());
}
// All locks released when `locked_entries` is dropped
```

## License
Expand Down
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
//! - Entry API for exclusive access to values
//! - Efficient concurrent operations through sharding
//! - Safe atomic updates
//! - No poisoning, the lock is released normally on panic
//!
//! # Examples
//! ```
Expand Down
13 changes: 13 additions & 0 deletions src/lockmap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,12 @@ impl<V> State<V> {
StateFlags(self.flags.load(Ordering::Acquire))
}

/// Increments the reference count.
///
/// # Note
///
/// The reference count uses 31 bits, supporting up to 2^31 concurrent references.
/// Overflow is not checked; exceeding this limit causes undefined behavior.
fn inc_ref(&self) -> StateFlags {
StateFlags(self.flags.fetch_add(1, Ordering::AcqRel) + 1)
}
Expand Down Expand Up @@ -261,6 +267,13 @@ impl<K: Eq + Hash, V> LockMap<K, V> {
/// * `Some(V)` if the key exists
/// * `None` if the key doesn't exist
///
/// # Performance Note
///
/// When no other thread holds an entry for this key, the `clone()` operation
/// is performed while holding the shard lock. If `V::clone()` is expensive,
/// consider using `entry()` or `entry_by_ref()` combined with `Entry::get()`
/// to avoid blocking other keys in the same shard.
///
/// **Locking behaviour:** Deadlock if called when holding the same entry.
///
/// # Examples
Expand Down
6 changes: 4 additions & 2 deletions src/shards_map.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use foldhash::fast::{FixedState, RandomState};
use foldhash::fast::RandomState;
use std::borrow::Borrow;
use std::collections::HashMap;
use std::hash::{BuildHasher, Hash};
Expand Down Expand Up @@ -195,6 +195,7 @@ where
pub struct ShardsMap<K, V> {
/// The vector of `ShardMap` instances.
shards: Vec<ShardMap<K, V>>,
hasher: RandomState,
}

impl<K, V> ShardsMap<K, V>
Expand All @@ -217,6 +218,7 @@ where
shards: (0..shard_amount)
.map(|_| ShardMap::with_capacity(shard_capacity))
.collect::<Vec<_>>(),
hasher: RandomState::default(),
}
}

Expand Down Expand Up @@ -311,7 +313,7 @@ where
K: Borrow<Q>,
Q: Eq + Hash + ?Sized,
{
let idx = FixedState::default().hash_one(key) as usize % self.shards.len();
let idx = self.hasher.hash_one(key) as usize % self.shards.len();
&self.shards[idx]
}
}
Expand Down
Loading