How does dashmap::DashMap::entry API compare to std::collections::HashMap::entry for atomic updates?

std::collections::HashMap::entry provides a single-threaded API for atomic get-or-insert operations within the context of exclusive access to the map. dashmap::DashMap::entry provides a similar API but operates within a sharded concurrent map where each entry operation only locks the relevant shard rather than the entire map. The key difference is that DashMap::entry enables concurrent access from multiple threads while still providing atomic updates to individual keys, whereas HashMap::entry requires wrapping the entire map in a Mutex or RwLock for concurrent access, which blocks all operations during the entry manipulation. However, DashMap::entry returns a guard that holds the shard lock, requiring careful attention to scope to avoid holding locks across await points or long computations.

Basic HashMap Entry API

use std::collections::HashMap;
 
fn hashmap_entry_basic() {
    let mut map = HashMap::new();
    
    // Insert if key doesn't exist
    map.entry("apple").or_insert(5);
    map.entry("apple").or_insert(10);  // Doesn't overwrite
    
    assert_eq!(map.get("apple"), Some(&5));
    
    // Modify if exists
    map.entry("apple").and_modify(|count| *count += 1).or_insert(1);
    assert_eq!(map.get("apple"), Some(&6));
    
    // Get or insert with computation
    let value = map.entry("banana").or_insert_with(|| {
        println!("Computing value for banana");
        42
    });
    assert_eq!(value, &42);
}

HashMap::entry provides atomic single-key operations without any concern for concurrent access.

Basic DashMap Entry API

use dashmap::DashMap;
 
fn dashmap_entry_basic() {
    let map = DashMap::new();
    
    // Same entry API, but thread-safe
    map.entry("apple").or_insert(5);
    map.entry("apple").or_insert(10);  // Doesn't overwrite
    
    assert_eq!(map.get("apple").map(|r| *r), Some(5));
    
    // Modify if exists
    map.entry("apple").and_modify(|count| *count += 1).or_insert(1);
    assert_eq!(map.get("apple").map(|r| *r), Some(6));
    
    // Get or insert with computation
    let value = map.entry("banana").or_insert_with(|| {
        println!("Computing value for banana");
        42
    });
    assert_eq!(*value, 42);
}

DashMap::entry provides the same API with concurrent access support.

Concurrency: HashMap with Mutex

use std::collections::HashMap;
use std::sync::{Arc, Mutex};
 
fn hashmap_concurrent() {
    let map = Arc::new(Mutex::new(HashMap::new()));
    
    // Thread 1
    let map1 = map.clone();
    let h1 = std::thread::spawn(move || {
        let mut map = map1.lock().unwrap();
        map.entry("key").or_insert(0);
        map.entry("key").and_modify(|v| *v += 1);
        // Lock held during entire operation
    });
    
    // Thread 2
    let map2 = map.clone();
    let h2 = std::thread::spawn(move || {
        let mut map = map2.lock().unwrap();
        // Must wait for Thread 1 to release lock
        map.entry("key").or_insert(0);
        map.entry("key").and_modify(|v| *v += 10);
    });
    
    h1.join().unwrap();
    h2.join().unwrap();
    
    let map = map.lock().unwrap();
    println!("Final value: {:?}", map.get("key"));
}

HashMap with Mutex blocks all threads during any entry operation on the entire map.

Concurrency: DashMap Sharded Locks

use dashmap::DashMap;
use std::sync::Arc;
 
fn dashmap_concurrent() {
    let map = Arc::new(DashMap::new());
    
    // Thread 1 - accesses "key1"
    let map1 = map.clone();
    let h1 = std::thread::spawn(move || {
        // Only locks the shard containing "key1"
        map1.entry("key1").or_insert(0);
        map1.entry("key1").and_modify(|v| *v += 1);
    });
    
    // Thread 2 - accesses "key2"
    let map2 = map.clone();
    let h2 = std::thread::spawn(move || {
        // Only locks the shard containing "key2"
        // Can run concurrently if "key1" and "key2" are in different shards
        map2.entry("key2").or_insert(0);
        map2.entry("key2").and_modify(|v| *v += 10);
    });
    
    // Thread 3 - also accesses "key1" (same shard as Thread 1)
    let map3 = map.clone();
    let h3 = std::thread::spawn(move || {
        // Must wait if same shard as Thread 1
        map3.entry("key1").and_modify(|v| *v += 100);
    });
    
    h1.join().unwrap();
    h2.join().unwrap();
    h3.join().unwrap();
    
    println!("key1: {:?}", map.get("key1").map(|r| *r));  // 101
    println!("key2: {:?}", map.get("key2").map(|r| *r));  // 10
}

DashMap uses sharded locking: different keys can be accessed concurrently if they're in different shards.

Entry Guards and Lock Duration

use dashmap::DashMap;
use std::sync::Arc;
 
fn dashmap_entry_guards() {
    let map = Arc::new(DashMap::new());
    map.insert("key", 0);
    
    // The entry() call returns an OccupiedEntry that holds the shard lock
    let entry = map.entry("key");
    
    // Shard is locked here - other operations on keys in same shard wait
    // This is similar to HashMap's Mutex, but only for one shard
    
    // The lock is released when entry goes out of scope
    {
        let mut entry = map.entry("key").or_insert(0);
        *entry += 1;
        // Lock still held while we use `entry`
    }
    // Lock released here
    
    // Pattern: release lock quickly
    map.entry("key").and_modify(|v| {
        // Quick modification while holding lock
        *v += 1;
    });
    // Lock released immediately after and_modify
}
 
fn dangerous_lock_patterns() {
    let map = Arc::new(DashMap::new());
    
    // DANGEROUS: Don't hold entry while doing slow operations
    if let dashmap::mapref::entry::Entry::Occupied(mut entry) = map.entry("key") {
        let value = entry.get_mut();
        // Don't do slow I/O here - shard is locked!
        // std::thread::sleep(std::time::Duration::from_secs(1));
        *value += 1;
    }
    
    // BETTER: Release lock, then do slow work
    let current = map.entry("key").or_insert(0).clone();
    // Lock released - now do slow work
    let new_value = expensive_computation(current);
    // Acquire lock again to update
    map.insert("key", new_value);
}
 
fn expensive_computation(n: i32) -> i32 {
    n + 1
}

DashMap::entry holds a shard lock; release it quickly to maximize concurrency.

Or Insert Behavior Comparison

use std::collections::HashMap;
use dashmap::DashMap;
 
fn or_insert_comparison() {
    // HashMap: single-threaded, no contention
    let mut hashmap = HashMap::new();
    hashmap.entry("key").or_insert(vec![1, 2, 3]);
    hashmap.entry("key").or_insert(vec![4, 5, 6]);  // Not inserted
    assert_eq!(hashmap.get("key"), Some(&vec![1, 2, 3]));
    
    // DashMap: concurrent-safe, same semantics
    let dashmap = DashMap::new();
    dashmap.entry("key").or_insert(vec![1, 2, 3]);
    dashmap.entry("key").or_insert(vec![4, 5, 6]);  // Not inserted
    assert_eq!(dashmap.get("key").map(|r| r.clone()), Some(vec![1, 2, 3]));
}
 
fn or_insert_with_comparison() {
    // HashMap: lazy initialization
    let mut hashmap = HashMap::new();
    let mut called = false;
    hashmap.entry("key").or_insert_with(|| {
        called = true;
        42
    });
    assert!(called);
    
    called = false;
    hashmap.entry("key").or_insert_with(|| {
        called = true;
        100
    });
    assert!(!called);  // Not called - key exists
    
    // DashMap: same lazy initialization, but thread-safe
    let dashmap = DashMap::new();
    let mut called = false;
    dashmap.entry("key").or_insert_with(|| {
        called = true;
        42
    });
    assert!(called);
}

Both provide identical semantics; DashMap adds thread safety.

And Modify for Updates

use std::collections::HashMap;
use dashmap::DashMap;
 
fn and_modify_comparison() {
    // HashMap: modify existing value
    let mut hashmap = HashMap::new();
    hashmap.insert("counter", 0);
    
    hashmap.entry("counter").and_modify(|v| *v += 1);
    assert_eq!(hashmap.get("counter"), Some(&1));
    
    // and_modify on missing key does nothing
    hashmap.entry("missing").and_modify(|v| *v += 1);
    assert!(!hashmap.contains_key("missing"));
    
    // DashMap: same behavior, concurrent-safe
    let dashmap = DashMap::new();
    dashmap.insert("counter", 0);
    
    dashmap.entry("counter").and_modify(|v| *v += 1);
    assert_eq!(dashmap.get("counter").map(|r| *r), Some(1));
}
 
fn and_modify_or_insert_pattern() {
    // Common pattern: increment or initialize
    let mut hashmap = HashMap::new();
    hashmap.entry("counter")
        .and_modify(|v| *v += 1)
        .or_insert(1);
    
    let dashmap = DashMap::new();
    dashmap.entry("counter")
        .and_modify(|v| *v += 1)
        .or_insert(1);
}

The and_modify pattern works identically in both APIs.

Entry Enum Variants

use std::collections::HashMap;
use dashmap::DashMap;
 
fn entry_variants_hashmap() {
    let mut map = HashMap::new();
    
    match map.entry("key") {
        std::collections::hash_map::Entry::Occupied(entry) => {
            println!("Key exists: {}", entry.key());
        }
        std::collections::hash_map::Entry::Vacant(entry) => {
            println!("Key missing, inserting default");
            entry.insert(42);
        }
    }
}
 
fn entry_variants_dashmap() {
    let map = DashMap::new();
    
    match map.entry("key") {
        dashmap::mapref::entry::Entry::Occupied(entry) => {
            println!("Key exists: {}", entry.key());
            // entry holds shard lock
        }
        dashmap::mapref::entry::Entry::Vacant(entry) => {
            println!("Key missing, inserting default");
            entry.insert(42);
            // entry holds shard lock
        }
    }
    // Shard lock released when entry goes out of scope
}

Both use the same Occupied/Vacant pattern; DashMap variants hold locks.

Real-World Example: Concurrent Counter

use dashmap::DashMap;
use std::sync::Arc;
 
fn concurrent_counter() {
    let counts = Arc::new(DashMap::new());
    
    let handles: Vec<_> = (0..100)
        .map(|i| {
            let counts = counts.clone();
            std::thread::spawn(move || {
                let key = format!("item_{}", i % 10);  // 10 different keys
                counts.entry(key)
                    .and_modify(|v| *v += 1)
                    .or_insert(1);
            })
        })
        .collect();
    
    for h in handles {
        h.join().unwrap();
    }
    
    // Each key should have count of 10
    for i in 0..10 {
        let key = format!("item_{}", i);
        assert_eq!(counts.get(&key).map(|r| *r), Some(10));
    }
}
 
// Compare with HashMap + Mutex
fn hashmap_mutex_counter() {
    use std::collections::HashMap;
    use std::sync::Mutex;
    
    let counts = Arc::new(Mutex::new(HashMap::new()));
    
    let handles: Vec<_> = (0..100)
        .map(|i| {
            let counts = counts.clone();
            std::thread::spawn(move || {
                let mut map = counts.lock().unwrap();
                let key = format!("item_{}", i % 10);
                map.entry(key)
                    .and_modify(|v| *v += 1)
                    .or_insert(1);
                // Lock held for entire operation
            })
        })
        .collect();
    
    for h in handles {
        h.join().unwrap();
    }
}

DashMap allows concurrent updates to different keys; HashMap + Mutex serializes all updates.

Real-World Example: Cache Population

use dashmap::DashMap;
use std::sync::Arc;
use std::time::Duration;
use std::thread;
 
fn cache_population() {
    let cache = Arc::new(DashMap::new());
    
    // Multiple threads populating cache
    let handles: Vec<_> = (0..10)
        .map(|thread_id| {
            let cache = cache.clone();
            thread::spawn(move || {
                for i in 0..10 {
                    let key = format!("key_{}", thread_id * 10 + i);
                    
                    // Only compute if not present
                    cache.entry(key.clone())
                        .or_insert_with(|| {
                            // Expensive computation
                            let value = expensive_lookup(&key);
                            value
                        });
                }
            })
        })
        .collect();
    
    for h in handles {
        h.join().unwrap();
    }
    
    println!("Cache size: {}", cache.len());
}
 
fn expensive_lookup(key: &str) -> String {
    // Simulate expensive lookup
    format!("value_for_{}", key)
}
 
// Key insight: or_insert_with is only called if key is missing
// The computation happens while holding the shard lock
// For very expensive computations, consider double-check pattern

For expensive computations in or_insert_with, the shard lock is held during computation.

Double-Check Pattern for Expensive Computations

use dashmap::DashMap;
use std::sync::Arc;
 
fn expensive_computation_cache() {
    let cache = Arc::new(DashMap::new());
    
    // Problem: or_insert_with holds lock during computation
    cache.entry("key").or_insert_with(|| {
        // This holds the shard lock during expensive computation
        // Other threads accessing same shard are blocked
        very_expensive_computation()
    });
    
    // Better: double-check pattern to minimize lock time
    let cache2 = Arc::new(DashMap::new());
    
    // First check without locking
    if let Some(value) = cache2.get("key") {
        // Already cached
        let _ = value;
    } else {
        // Not cached, compute outside of lock
        let computed = very_expensive_computation();
        
        // Now try to insert
        cache2.entry("key")
            .or_insert_with(|| {
                // Another thread might have inserted by now
                // Return our computed value, but it might not be used
                computed.clone()
            });
    }
    
    // Even better: use the entry pattern carefully
    fn get_or_compute(cache: &DashMap<String, String>, key: &str) -> String {
        // Check if exists
        if let Some(value) = cache.get(key) {
            return value.clone();
        }
        
        // Compute outside lock
        let value = very_expensive_computation();
        
        // Insert - might race with another thread
        cache.entry(key.to_string())
            .or_insert_with(|| value.clone())
            .clone()
    }
}
 
fn very_expensive_computation() -> String {
    "expensive_result".to_string()
}

For expensive computations, consider patterns that minimize time holding the shard lock.

Limitations of DashMap Entry

use dashmap::DashMap;
 
fn dashmap_entry_limitations() {
    let map = DashMap::new();
    map.insert("a", 1);
    map.insert("b", 2);
    
    // Cannot hold two entries at once (might deadlock on same shard)
    // This would be dangerous:
    // let entry_a = map.entry("a");
    // let entry_b = map.entry("b");  // Might deadlock if same shard
    
    // Safe: complete one entry operation before another
    {
        let mut entry = map.entry("a").or_insert(0);
        *entry += 1;
    }  // Entry dropped, lock released
    
    {
        let mut entry = map.entry("b").or_insert(0);
        *entry += 1;
    }
    
    // HashMap with Mutex has the same limitation
    // but at the whole-map level instead of shard level
}
 
// Deadlock risk demonstration
fn potential_deadlock() {
    use std::sync::Arc;
    
    let map = Arc::new(DashMap::new());
    
    // If these keys hash to the same shard, this could deadlock
    // in a multi-threaded scenario where threads try to acquire
    // entries in different orders
    
    // Thread 1
    let map1 = map.clone();
    let h1 = std::thread::spawn(move || {
        // Hold entry for "a"
        let _e1 = map1.entry("a").or_insert(0);
        // Try to get entry for "b"
        // If same shard, we're trying to lock twice - this works
        // in DashMap because it's reentrant for the same thread
        let _e2 = map1.entry("b").or_insert(0);
    });
    
    h1.join().unwrap();
}

Be careful about holding multiple entries simultaneously; they might lock the same shard.

Transaction-Like Operations

use dashmap::DashMap;
use std::collections::HashMap;
 
// HashMap: easy to make atomic multi-key operations
fn hashmap_multi_key_atomic() {
    let mut map = HashMap::new();
    
    // Single lock protects entire operation
    map.entry("a").or_insert(0);
    map.entry("b").or_insert(0);
    
    // Both entries modified atomically (single-threaded)
    if let Some(a) = map.get_mut("a") {
        *a += 1;
    }
    if let Some(b) = map.get_mut("b") {
        *b += 1;
    }
}
 
// DashMap: multi-key operations are NOT atomic across keys
fn dashmap_multi_key_not_atomic() {
    let map = DashMap::new();
    
    // Each entry operation locks a different shard
    map.entry("a").or_insert(0);  // Locks shard for "a"
    map.entry("b").or_insert(0);  // Locks shard for "b" (maybe different)
    
    // These are NOT atomic together
    // Another thread could see "a" updated but "b" not yet updated
    
    // For atomic multi-key operations, you need external synchronization
}
 
// If you need atomic multi-key operations with DashMap
fn dashmap_with_external_sync() {
    use std::sync::Mutex;
    
    let map = DashMap::new();
    let multi_key_lock = Mutex::new(());
    
    // Lock for multi-key operation
    let _guard = multi_key_lock.lock().unwrap();
    
    // Now safe to do multi-key operations
    map.entry("a").and_modify(|v| *v += 1);
    map.entry("b").and_modify(|v| *v += 1);
    
    // But this reduces concurrency for those keys
}

DashMap doesn't provide atomic multi-key operations; use external synchronization if needed.

Comparison Summary

Aspect HashMap::entry DashMap::entry
Thread safety None (single-threaded) Sharded locking
Concurrency N/A (single-threaded) Concurrent access to different shards
Lock granularity N/A Per-shard
Lock scope N/A During entry operation
Multi-key atomicity Yes (single lock) No (different shards)
Memory overhead Lower Higher (shards + overhead)
API similarity Standard Same API, concurrent semantics

Synthesis

The DashMap::entry API provides the same ergonomic interface as HashMap::entry but with concurrent access semantics that enable multi-threaded usage without a global lock:

Key similarities:

  • Same or_insert, or_insert_with, and_modify methods
  • Same Occupied/Vacant entry variants
  • Same atomic semantics for single-key operations

Key differences:

  • DashMap::entry holds a shard lock; HashMap::entry has no locking concern
  • DashMap allows concurrent access to different keys; HashMap + Mutex blocks all access
  • DashMap cannot provide atomic multi-key operations; HashMap naturally does
  • DashMap entry guards must be released quickly; HashMap has no such concern

When to use DashMap::entry:

  • Multiple threads updating a shared map
  • Keys are independent (no multi-key transactions needed)
  • You want fine-grained locking without managing it yourself
  • Read and write operations are interleaved

When to use HashMap::entry:

  • Single-threaded context
  • Need atomic multi-key operations
  • Simplicity is more important than concurrency
  • Using Mutex<HashMap> when all updates need global synchronization

Key insight: DashMap::entry is not a drop-in replacement for HashMap::entry in concurrent code—it's better. A Mutex<HashMap> blocks all threads on any entry operation, while DashMap only blocks threads accessing the same shard. However, this finer granularity means you cannot atomically modify multiple keys; each entry operation is atomic only for that single key. Choose based on whether you need single-key or multi-key atomicity.