How does dashmap::DashMap::entry API compare to HashMap::entry for concurrent modification patterns?

DashMap::entry provides a concurrent-safe entry API that uses fine-grained shard-level locking, allowing multiple threads to modify different keys concurrently while maintaining atomicity guarantees—unlike HashMap::entry which requires wrapping the entire map in a Mutex or RwLock, serializing all modifications. The key difference is that DashMap::entry returns an Entry that holds only a lock on the relevant shard, while HashMap::entry is purely sequential and requires external synchronization for concurrent access. DashMap::entry supports the same or_insert and or_insert_with patterns but with concurrent-safe semantics.

The Concurrency Challenge with HashMap::entry

use std::collections::HashMap;
use std::sync::{Arc, Mutex};
 
// HashMap::entry requires external synchronization
fn hashmap_entry_problem() {
    let map = Arc::new(Mutex::new(HashMap::<String, i32>::new()));
    
    // Multiple threads want to insert/update concurrently
    let mut handles = vec![];
    
    for i in 0..10 {
        let map_clone = Arc::clone(&map);
        let handle = std::thread::spawn(move || {
            let mut map = map_clone.lock().unwrap();
            
            // This locks the ENTIRE map for the duration of entry operations
            map.entry(format!("key_{}", i % 3))
                .or_insert_with(|| {
                    // While this runs, no other thread can access ANY key
                    i
                });
        });
        handles.push(handle);
    }
    
    for handle in handles {
        handle.join().unwrap();
    }
    
    // Problem: Only one thread can hold the lock at a time
    // Even if they're working on different keys
}

With HashMap::entry, a mutex lock covers the entire map, serializing all operations.

DashMap::entry with Fine-Grained Locking

use dashmap::DashMap;
use std::sync::Arc;
 
fn dashmap_entry_solution() {
    let map = Arc::new(DashMap::<String, i32>::new());
    
    let mut handles = vec![];
    
    for i in 0..10 {
        let map_clone = Arc::clone(&map);
        let handle = std::thread::spawn(move || {
            // DashMap locks only the shard containing this key
            // Different keys may be in different shards
            // Operations on different shards proceed concurrently
            map_clone.entry(format!("key_{}", i % 3))
                .or_insert_with(|| i);
        });
        handles.push(handle);
    }
    
    for handle in handles {
        handle.join().unwrap();
    }
    
    // Multiple threads can work concurrently on different keys
    // Each shard has its own lock
}

DashMap internally shards the map into multiple segments, each with its own lock.

Basic Entry API Comparison

use std::collections::HashMap;
use dashmap::DashMap;
 
fn basic_entry_comparison() {
    // HashMap entry API (sequential)
    let mut hashmap = HashMap::new();
    
    hashmap.entry("key1".to_string())
        .or_insert(1);
    
    hashmap.entry("key2".to_string())
        .or_insert_with(|| {
            // Computation only runs if key doesn't exist
            2 + 2
        });
    
    // DashMap entry API (concurrent)
    let dashmap = DashMap::new();
    
    dashmap.entry("key1".to_string())
        .or_insert(1);
    
    dashmap.entry("key2".to_string())
        .or_insert_with(|| {
            // This closure runs while holding only the shard lock
            // Not the entire map lock
            2 + 2
        });
}

Both APIs use the same patterns; DashMap adds concurrent-safe semantics.

Entry Operations with Atomic Semantics

use dashmap::DashMap;
 
fn atomic_entry_operations() {
    let map = DashMap::new();
    
    // or_insert: Insert if missing
    map.entry("counter".to_string())
        .or_insert(0);
    
    // or_insert_with: Lazy insertion
    map.entry("computed".to_string())
        .or_insert_with(|| {
            // This computation is atomic with the lookup
            // No other thread can insert between the lookup and insert
            expensive_computation()
        });
    
    // and_modify: Modify existing value atomically
    map.entry("counter".to_string())
        .and_modify(|count| *count += 1);
    
    // Chaining operations
    map.entry("counter".to_string())
        .or_insert(0)
        .and_modify(|count| *count += 1);
    
    // The entry is held during these operations
    // No race conditions with other threads
}
 
fn expensive_computation() -> i32 {
    42
}

DashMap::entry ensures atomicity between checking existence and inserting/updating.

Concurrent Counter Pattern

use dashmap::DashMap;
use std::sync::Arc;
 
fn concurrent_counter() {
    let counters = Arc::new(DashMap::<String, i32>::new());
    
    let mut handles = vec![];
    
    // Multiple threads incrementing counters concurrently
    for _ in 0..100 {
        let counters_clone = Arc::clone(&counters);
        let handle = std::thread::spawn(move || {
            // Each thread increments different counters
            for key in &["a", "b", "c"] {
                counters_clone
                    .entry(key.to_string())
                    .and_modify(|count| *count += 1)
                    .or_insert(1);
            }
        });
        handles.push(handle);
    }
    
    for handle in handles {
        handle.join().unwrap();
    }
    
    // Each counter has been incremented correctly
    // No lost updates due to race conditions
    println!("a: {:?}", counters.get("a"));
}
 
// Compare with HashMap + Mutex approach
use std::collections::HashMap;
use std::sync::Mutex;
 
fn hashmap_counter_problem() {
    let counters = Arc::new(Mutex::new(HashMap::<String, i32>::new()));
    
    let mut handles = vec![];
    
    for _ in 0..100 {
        let counters_clone = Arc::clone(&counters);
        let handle = std::thread::spawn(move || {
            // This approach has problems:
            
            // Problem 1: Lock contention
            let mut map = counters_clone.lock().unwrap();
            
            // All threads wait for this lock
            // Even if modifying different keys
            for key in &["a", "b", "c"] {
                *map.entry(key.to_string()).or_insert(0) += 1;
            }
            
            // Lock is held for entire loop, not just for one key
        });
        handles.push(handle);
    }
    
    // Result is correct, but much slower due to lock contention
}

DashMap::entry provides concurrent access with minimal contention.

Entry API Patterns

use dashmap::DashMap;
 
fn entry_patterns() {
    let map: DashMap<String, Vec<i32>> = DashMap::new();
    
    // Pattern 1: Insert or get
    let value = map.entry("key1".to_string())
        .or_insert(vec![1, 2, 3]);
    
    // Pattern 2: Insert or compute
    map.entry("key2".to_string())
        .or_insert_with(|| {
            // Compute only if needed
            (1..=10).collect()
        });
    
    // Pattern 3: Modify if exists
    map.entry("key1".to_string())
        .and_modify(|vec| {
            vec.push(4);
        });
    
    // Pattern 4: Insert default, then modify
    map.entry("key3".to_string())
        .or_insert_with(Vec::new)
        .push(1);
    
    // Pattern 5: Conditional insert
    if let dashmap::mapref::entry::Entry::Vacant(e) = map.entry("key4".to_string()) {
        e.insert(vec![42]);
    }
    
    // Pattern 6: Get or create with complex value
    map.entry("key5".to_string())
        .or_insert_with(|| {
            let mut v = Vec::new();
            v.extend(0..5);
            v
        });
}

The same entry patterns from HashMap work with DashMap.

OccupiedEntry and VacantEntry

use dashmap::DashMap;
 
fn occupied_vacant_entry() {
    let map: DashMap<String, i32> = DashMap::new();
    
    // Entry can be either Vacant or Occupied
    use dashmap::mapref::entry::Entry;
    
    match map.entry("key1".to_string()) {
        Entry::Vacant(e) => {
            // Key doesn't exist
            // Can insert
            e.insert(1);
        }
        Entry::Occupied(e) => {
            // Key exists
            // Can get, modify, or remove
            let existing = e.get();
            e.insert(*existing + 1);
        }
    }
    
    // Working with OccupiedEntry
    map.insert("key2".to_string(), 10);
    
    if let Entry::Occupied(mut e) = map.entry("key2".to_string()) {
        // Get current value
        let current = *e.get();
        
        // Modify in place
        e.insert(current + 1);
        
        // Remove entry
        // e.remove_entry();
    }
    
    // Working with VacantEntry
    if let Entry::Vacant(e) = map.entry("key3".to_string()) {
        e.insert(100);
    }
}

DashMap::entry returns Entry<Vacant, Occupied> variants with concurrent-safe semantics.

Concurrent Caching Pattern

use dashmap::DashMap;
use std::sync::Arc;
use std::time::Instant;
 
fn concurrent_cache() {
    let cache = Arc::new(DashMap::<String, String>::new());
    
    let mut handles = vec![];
    
    for i in 0..10 {
        let cache_clone = Arc::clone(&cache);
        let handle = std::thread::spawn(move || {
            let key = format!("item_{}", i % 5);
            
            // Check cache, compute if missing
            let value = cache_clone
                .entry(key.clone())
                .or_insert_with(|| {
                    // This computation happens only once per key
                    // Even if multiple threads try simultaneously
                    println!("Computing {}...", key);
                    expensive_fetch(&key)
                });
            
            // Use cached value
            println!("Thread {} got: {}", i, value);
        });
        handles.push(handle);
    }
    
    for handle in handles {
        handle.join().unwrap();
    }
    
    // Each key is computed exactly once
    // Other threads wait and use cached result
}
 
fn expensive_fetch(key: &str) -> String {
    // Simulate expensive operation
    std::thread::sleep(std::time::Duration::from_millis(100));
    format!("value_for_{}", key)
}

DashMap::entry is ideal for concurrent memoization and caching.

Lock Granularity Comparison

use std::collections::HashMap;
use std::sync::{Arc, RwLock};
use dashmap::DashMap;
 
fn lock_granularity_comparison() {
    // HashMap with RwLock: Single lock for entire map
    let hashmap = Arc::new(RwLock::new(HashMap::<String, i32>::new()));
    
    // DashMap: Multiple shard locks (default: number of CPUs * 4)
    let dashmap = Arc::new(DashMap::<String, i32>::new());
    
    // With HashMap + RwLock:
    // - Write operations block ALL other operations
    // - Read operations can proceed concurrently
    // - Entry modification requires write lock
    // - Entire map is locked during entry operations
    
    {
        let mut map = hashmap.write().unwrap();
        // Entire map is locked
        // No other thread can read OR write
        map.entry("key1".to_string()).or_insert(1);
        // Lock released at end of scope
    }
    
    // With DashMap:
    // - Map is divided into shards (default 16+)
    // - Each shard has its own lock
    // - Operations on different shards proceed concurrently
    // - Entry operations lock only one shard
    
    dashmap.entry("key1".to_string()).or_insert(1);
    // Only the shard containing "key1" is locked during this operation
    // Operations on other shards proceed concurrently
    
    // Benefit: Higher concurrency for operations on different keys
    // Keys that hash to different shards can be modified concurrently
}

DashMap uses fine-grained locking; HashMap with RwLock uses coarse-grained locking.

Shard Selection

use dashmap::DashMap;
use std::hash::{Hash, Hasher};
 
fn shard_selection() {
    // DashMap determines shard based on key hash
    let map: DashMap<String, i32> = DashMap::new();
    
    // By default, DashMap has shards = (number of CPUs * 4)
    // This can be customized:
    let custom_map: DashMap<String, i32> = 
        DashMap::with_shard_amount(32);
    
    // Shard selection: hash(key) % number_of_shards
    // Keys hashing to the same shard contend for the same lock
    // Keys hashing to different shards can be accessed concurrently
    
    // Implication:
    // - Uniform key distribution = good concurrency
    // - Poor hash function = hot spots
    // - Custom shard count for specific workloads
    
    // Example: If many operations target similar keys
    // They'll contend for the same shard lock
    // But different keys proceed concurrently
}

Shard count affects concurrency; keys in different shards are accessed concurrently.

Performance Characteristics

use dashmap::DashMap;
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
 
fn performance_comparison() {
    // HashMap + Mutex:
    // - Single lock for entire map
    // - All operations serialized
    // - Good for read-heavy with few writes
    // - Poor for write-heavy or mixed workload
    
    // HashMap + RwLock:
    // - Read lock allows concurrent readers
    // - Write lock blocks all readers and writers
    // - Good for read-heavy workloads
    // - Writer starvation possible
    
    // DashMap:
    // - Per-shard locks (multiple locks)
    // - Operations on different shards concurrent
    // - Good for mixed read-write workloads
    // - Scales with number of shards
    
    // Memory overhead:
    // - HashMap: Minimal
    // - DashMap: Multiple hash tables (one per shard)
    // - Trade-off: Memory for concurrency
}

DashMap trades memory for concurrency; HashMap with locks trades concurrency for simplicity.

Entry and References

use dashmap::DashMap;
 
fn entry_references() {
    let map: DashMap<String, Vec<i32>> = DashMap::new();
    
    // or_insert returns a reference wrapper
    let entry = map.entry("key".to_string())
        .or_insert(vec![1, 2, 3]);
    
    // The reference is a RwLockReadGuard wrapper
    // It derefs to the value
    
    // To modify, you need to go through entry again
    // or use and_modify
    
    map.entry("key".to_string())
        .and_modify(|vec| {
            vec.push(4);  // Modify in place
        });
    
    // Alternative: get the value and modify through reference
    if let Some(mut value) = map.get_mut("key") {
        value.push(5);
    }
    
    // Important: Entry holds a shard lock
    // Long-running operations block other operations on same shard
    // Keep entry operations short
}

Entry operations hold shard locks; keep operations short to avoid contention.

Compare-and-Set Pattern

use dashmap::DashMap;
 
fn compare_and_set() {
    let map: DashMap<String, i32> = DashMap::new();
    
    // Pattern: Only update if condition holds
    fn update_if_greater(map: &DashMap<String, i32>, key: &str, new_value: i32) {
        map.entry(key.to_string())
            .and_modify(|current| {
                if new_value > *current {
                    *current = new_value;
                }
            })
            .or_insert(new_value);
    }
    
    update_if_greater(&map, "score", 10);
    update_if_greater(&map, "score", 5);   // Won't update (5 < 10)
    update_if_greater(&map, "score", 15);  // Updates (15 > 10)
    
    // This is atomic at the entry level
    // No race condition between read and write
}

and_modify enables atomic compare-and-set operations.

Complex Concurrent Modification

use dashmap::DashMap;
use std::sync::Arc;
 
struct UserStats {
    logins: u32,
    last_login: Option<String>,
    actions: Vec<String>,
}
 
fn complex_modification() {
    let stats: DashMap<String, UserStats> = DashMap::new();
    
    // Complex entry update
    fn record_login(map: &DashMap<String, UserStats>, user: &str, timestamp: &str) {
        map.entry(user.to_string())
            .and_modify(|stats| {
                stats.logins += 1;
                stats.last_login = Some(timestamp.to_string());
            })
            .or_insert_with(|| UserStats {
                logins: 1,
                last_login: Some(timestamp.to_string()),
                actions: Vec::new(),
            });
    }
    
    // Record action
    fn record_action(map: &DashMap<String, UserStats>, user: &str, action: &str) {
        map.entry(user.to_string())
            .or_insert_with(|| UserStats {
                logins: 0,
                last_login: None,
                actions: Vec::new(),
            })
            .actions
            .push(action.to_string());
        
        // Note: This requires special handling because
        // or_insert returns a reference that we can't easily mutate
    }
    
    // Correct approach using and_modify
    fn record_action_correct(map: &DashMap<String, UserStats>, user: &str, action: &str) {
        map.entry(user.to_string())
            .and_modify(|stats| {
                stats.actions.push(action.to_string());
            })
            .or_insert_with(|| UserStats {
                logins: 0,
                last_login: None,
                actions: vec![action.to_string()],
            });
    }
}

Use and_modify with or_insert_with for complex atomic updates.

Limitations and Considerations

use dashmap::DashMap;
 
fn limitations() {
    let map: DashMap<String, i32> = DashMap::new();
    
    // Limitation 1: Entry operations lock a shard
    // Don't hold entry for long operations
    // This blocks all keys in that shard
    
    // Limitation 2: Can't return mutable reference from entry easily
    // Entry::or_insert returns a reference
    // For mutation, use and_modify or get_mut separately
    
    // Limitation 3: Cross-key operations not atomic
    // If you need to update multiple keys atomically,
    // DashMap can't guarantee that
    // You'd need external synchronization
    
    // Limitation 4: Iteration locks all shards
    // map.iter() acquires all locks
    // Writers are blocked during iteration
    
    // Limitation 5: No equivalent to HashMap::entry's Entry::key()
    // DashMap's Entry doesn't expose the key directly
    // (This varies by version - check documentation)
}

Understand DashMap limitations for correct concurrent code.

When to Use Each Approach

use std::collections::HashMap;
use std::sync::{Arc, Mutex, RwLock};
use dashmap::DashMap;
 
fn choosing_approach() {
    // Use DashMap when:
    // - Multiple threads need concurrent access
    // - Mixed read-write workload
    // - Operations on different keys need concurrent access
    // - You want fine-grained locking without managing it yourself
    
    // Use HashMap + RwLock when:
    // - Read-heavy workload (many readers, few writers)
    // - Simple use case
    // - You don't need fine-grained concurrency
    
    // Use HashMap + Mutex when:
    // - Very simple synchronization needs
    // - Low contention expected
    // - Simplicity is priority
    
    // Use HashMap (no sync) when:
    // - Single-threaded context
    // - Thread-local storage
    // - Performance is critical and no concurrency needed
}

Choose based on workload characteristics and concurrency requirements.

Synthesis

Key differences:

// HashMap::entry (sequential)
// - No concurrency support
// - Requires external synchronization (Mutex/RwLock)
// - Lock covers entire map
// - Simple API, familiar to Rust users
 
// DashMap::entry (concurrent)
// - Built-in concurrency support
// - Fine-grained shard locking
// - Lock covers only affected shard
// - Same API patterns, concurrent semantics

Entry API comparison:

use std::collections::HashMap;
use dashmap::DashMap;
 
// Both support:
// - entry(key).or_insert(value)
// - entry(key).or_insert_with(|| value)
// - entry(key).and_modify(|v| ...)
// - Entry::Occupied / Entry::Vacant variants
 
// HashMap: Entry operations are atomic because they're sequential
let mut map: HashMap<String, i32> = HashMap::new();
map.entry("key".to_string()).or_insert(1);
 
// DashMap: Entry operations are atomic within a shard
let map: DashMap<String, i32> = DashMap::new();
map.entry("key".to_string()).or_insert(1);
// Other shards can be accessed concurrently

Concurrency model:

// HashMap + Mutex/RwLock: Coarse-grained
// - One lock for entire map
// - All operations serialized (Mutex) or readers serialized with writers (RwLock)
// - Simple to reason about
// - Can bottleneck on high contention
 
// DashMap: Fine-grained (sharded)
// - Multiple locks, one per shard
// - Operations on different shards concurrent
// - Better scalability under contention
// - Slightly more complex (shard selection matters)

Key insight: DashMap::entry provides the same entry API patterns as HashMap::entry—or_insert, or_insert_with, and_modify, and Occupied/Vacant variants—but with concurrent-safe semantics using fine-grained shard locking instead of requiring external synchronization. While HashMap::entry requires wrapping the entire map in a Mutex or RwLock (serializing all modifications), DashMap::entry automatically locks only the shard containing the target key, allowing concurrent modifications to keys in different shards. This makes DashMap::entry ideal for concurrent caching, memoization, and counter patterns where multiple threads need to insert or update values atomically without blocking operations on unrelated keys. The entry API ensures atomicity between checking for existence and inserting/updating—the classic "check-then-insert" race condition is handled internally by holding the shard lock during the entire entry operation. Use DashMap::entry for concurrent modification patterns; use HashMap::entry with external synchronization when simplicity matters more than concurrency, or in single-threaded contexts.