What are the trade-offs between dashmap::DashMap::entry and get_mut for conditional insertion patterns?
DashMap::entry provides atomic conditional insertion that holds a lock only during the operation, while get_mut returns a reference guard that holds a lock for the entire scopeāentry is ideal for single atomic operations like "insert if absent," and get_mut is necessary when you need to perform multiple operations on a value while holding the lock, but careful scope management is required to avoid deadlocks. Understanding when to use each is critical for concurrent hash map performance and correctness.
DashMap Basics
use dashmap::DashMap;
fn dashmap_basics() {
// DashMap is a concurrent hash map that allows lock-free reads
// and fine-grained locking for writes
let map = DashMap::new();
// Basic operations
map.insert("key", "value");
// Read (lock-free)
if let Some(value) = map.get("key") {
println!("Got: {}", value);
}
// Write (locks the shard containing the key)
if let Some(mut value) = map.get_mut("key") {
*value = "new_value";
}
// Entry API for conditional insertion
map.entry("key").or_insert("default");
}DashMap shards data across multiple internal maps for concurrent access.
The Entry API Approach
use dashmap::DashMap;
fn entry_patterns() {
let map = DashMap::new();
// Pattern 1: Insert if absent
map.entry("key1").or_insert("default");
// Lock is held only during this operation
// Pattern 2: Insert with computed value
map.entry("key2").or_insert_with(|| {
// Computation only happens if key is absent
"computed_value"
});
// Pattern 3: Insert with async computation (requires tokio)
// map.entry("key3").or_insert_with_async(async { "async_value" }).await;
// Pattern 4: Get existing or insert default
let value = map.entry("key4").or_insert("default");
// Returns reference to value (still holding lock!)
// This is important: the lock is held while you use the value
// Pattern 5: Modify existing or insert default
map.entry("key5").and_modify(|v| *v = "modified").or_insert("new");
// Pattern 6: Check if occupied before insertion
if let dashmap::mapref::entry::Entry::Vacant(e) = map.entry("key6") {
e.insert("only_if_absent");
}
}The entry API provides atomic conditional operations with automatic lock management.
The get_mut Approach
use dashmap::DashMap;
fn get_mut_patterns() {
let map = DashMap::new();
map.insert("key", 0);
// get_mut returns a reference guard that holds a lock
if let Some(mut value) = map.get_mut("key") {
// Lock is held for entire scope
*value += 1;
*value *= 2;
// Multiple modifications are efficient under one lock
}
// Lock released here
// Problem: lock is held for entire scope
{
if let Some(mut value) = map.get_mut("key") {
// Lock held during this entire block
std::thread::sleep(std::time::Duration::from_millis(100));
*value += 1;
}
}
// For conditional insertion, get_mut is problematic:
if map.get_mut("key").is_none() {
// Key doesn't exist... but another thread might insert!
map.insert("key", 1); // Race condition!
}
}get_mut holds a lock for the entire scope, which can be good or bad depending on context.
Key Trade-off: Lock Duration
use dashmap::DashMap;
use std::sync::Arc;
use std::thread;
fn lock_duration_comparison() {
let map = Arc::new(DashMap::new());
// Scenario: High-contention key
// BAD: get_mut holds lock during computation
let map_clone = map.clone();
let handle = thread::spawn(move || {
if let Some(mut v) = map_clone.get_mut("key") {
// Lock held during entire computation
let computed = expensive_computation(); // Takes 100ms
*v = computed;
}
});
// Concurrent access to other keys is fine
// But access to same key is blocked for 100ms
// GOOD: entry releases lock quickly
let map_clone = map.clone();
let handle = thread::spawn(move || {
map_clone.entry("key").or_insert_with(|| {
expensive_computation() // Still computed, but lock released faster
});
});
}
fn expensive_computation() -> i32 {
std::thread::sleep(std::time::Duration::from_millis(100));
42
}entry is designed for minimal lock holding; get_mut locks for the entire scope.
Conditional Insertion: entry vs get_mut
use dashmap::DashMap;
fn conditional_insertion() {
let map = DashMap::new();
// GOAL: Insert only if key doesn't exist
// APPROACH 1: entry API (CORRECT)
map.entry("key").or_insert("value");
// Atomic: check-and-insert happens atomically
// APPROACH 2: get_mut (WRONG - race condition)
if map.get_mut("key").is_none() {
// PROBLEM: Between the check and insert, another thread
// might insert the same key!
map.insert("key", "value"); // Overwrites or duplicates
}
// APPROACH 3: get then insert (ALSO WRONG)
if !map.contains_key("key") {
// Same race condition: another thread might insert here
map.insert("key", "value");
}
}For "insert if absent," entry is the correct choice; manual check-then-insert has race conditions.
Multiple Modifications: When get_mut Makes Sense
use dashmap::DashMap;
fn multiple_modifications() {
let map = DashMap::new();
map.insert("counter", 0);
// GOOD use case for get_mut: multiple operations on same value
if let Some(mut counter) = map.get_mut("counter") {
// All modifications happen under one lock acquisition
*counter += 1;
if *counter > 10 {
*counter = 0; // Reset if too high
}
// Lock held for entire block, but that's intentional
}
// BAD: Using entry for multiple modifications
// Each entry call acquires and releases lock
map.entry("counter").and_modify(|c| *c += 1);
map.entry("counter").and_modify(|c| {
if *c > 10 {
*c = 0;
}
});
// Two lock acquisitions instead of one
}get_mut is appropriate when you need multiple modifications under one lock.
Deadlock Scenarios
use dashmap::DashMap;
use std::sync::Arc;
use std::thread;
fn deadlock_scenario() {
let map1 = Arc::new(DashMap::new());
let map2 = Arc::new(DashMap::new());
map1.insert("key", "value");
map2.insert("key", "value");
let map1_clone = map1.clone();
let map2_clone = map2.clone();
// DANGER: get_mut can cause deadlocks with nested locking
let handle1 = thread::spawn(move || {
if let Some(mut v1) = map1_clone.get_mut("key") {
// Hold lock on map1
std::thread::sleep(std::time::Duration::from_millis(10));
// Try to get lock on map2 while holding map1 lock
if let Some(mut v2) = map2_clone.get_mut("key") {
*v1 = "new1";
*v2 = "new2";
}
}
});
let handle2 = thread::spawn(move || {
if let Some(mut v2) = map2.get_mut("key") {
// Hold lock on map2
std::thread::sleep(std::time::Duration::from_millis(10));
// Try to get lock on map1 while holding map2 lock
if let Some(mut v1) = map1.get_mut("key") {
*v1 = "new1";
*v2 = "new2";
}
}
});
// DEADLOCK: Thread 1 holds map1, waiting for map2
// Thread 2 holds map2, waiting for map1
// entry API wouldn't help here either if nesting
// Solution: Always acquire locks in same order, or use try_lock patterns
}Both entry and get_mut can deadlock with nested locks; entry reduces the window.
Performance Comparison
use dashmap::DashMap;
use std::sync::Arc;
use std::thread;
fn performance_comparison() {
let map = Arc::new(DashMap::new());
// Initialize with data
for i in 0..1000 {
map.insert(format!("key{}", i), 0);
}
// Scenario 1: Many threads doing conditional insert
let map_clone = map.clone();
let start = std::time::Instant::now();
let handles: Vec<_> = (0..10)
.map(|t| {
let map = map_clone.clone();
thread::spawn(move || {
for i in 0..1000 {
// entry API: one lock acquisition per insert
map.entry(format!("key{}", i)).or_insert(0);
}
})
})
.collect();
for handle in handles {
handle.join().unwrap();
}
let entry_time = start.elapsed();
// Scenario 2: Many threads doing check-then-insert (wrong pattern)
let map_clone = map.clone();
let start = std::time::Instant::now();
let handles: Vec<_> = (0..10)
.map(|t| {
let map = map_clone.clone();
thread::spawn(move || {
for i in 0..1000 {
// Wrong pattern: two operations
if map.get_mut(format!("key{}", i).as_str()).is_none() {
map.insert(format!("key{}", i), 0);
}
}
})
})
.collect();
for handle in handles {
handle.join().unwrap();
}
let check_insert_time = start.elapsed();
println!("entry: {:?}", entry_time);
println!("check-then-insert: {:?}", check_insert_time);
// entry is typically faster due to single lock acquisition
}entry is more efficient for conditional insertion due to single atomic operation.
Entry API Variants
use dashmap::DashMap;
fn entry_variants() {
let map = DashMap::new();
// or_insert: Insert if vacant
map.entry("key1").or_insert("default");
// or_insert_with: Lazy computation
map.entry("key2").or_insert_with(|| {
// Only called if key doesn't exist
expensive_value()
});
// or_default: Insert Default::default()
map.entry("key3").or_default();
// Inserts "" for String, 0 for i32, etc.
// and_modify: Modify existing value
map.entry("key1").and_modify(|v| *v = "modified");
// Chaining: modify existing or insert new
map.entry("key4")
.and_modify(|v| *v = "updated")
.or_insert("new");
// Entry enum for fine control
match map.entry("key5") {
dashmap::mapref::entry::Entry::Occupied(entry) => {
println!("Key exists: {}", entry.get());
}
dashmap::mapref::entry::Entry::Vacant(entry) => {
entry.insert("new_value");
}
}
}
fn expensive_value() -> &'static str {
// Simulate expensive computation
"computed"
}The entry API provides rich operations for conditional insertion patterns.
get_mut for Complex Updates
use dashmap::DashMap;
fn complex_updates() {
let map = DashMap::new();
map.insert("account", Account { balance: 100, transactions: 0 });
// Complex update requiring multiple modifications
if let Some(mut account) = map.get_mut("account") {
// All modifications happen atomically under one lock
account.balance -= 10;
account.transactions += 1;
// Can read multiple times without reacquiring lock
if account.balance < 0 {
account.balance = 0; // Overdraft protection
}
// Computation can use current state
let fee = if account.balance > 1000 { 1 } else { 5 };
account.balance -= fee;
}
// Lock released here
// This would be awkward with entry API:
// map.entry("account").and_modify(|a| {
// a.balance -= 10;
// a.transactions += 1;
// if a.balance < 0 {
// a.balance = 0;
// }
// let fee = if a.balance > 1000 { 1 } else { 5 };
// a.balance -= fee;
// });
// Works but harder to read
}
struct Account {
balance: i32,
transactions: u32,
}For complex multi-step updates, get_mut is cleaner than nested and_modify.
Reference Guard Behavior
use dashmap::DashMap;
fn reference_guard_behavior() {
let map = DashMap::new();
map.insert("key", "value");
// get returns a reference guard
let value = map.get("key").unwrap();
// Lock is held on the shard containing "key"
// The guard keeps the lock alive
// get_mut returns a mutable reference guard
let mut_value = map.get_mut("key").unwrap();
// Same lock behavior, but mutable
// entry returns different guard types
use dashmap::mapref::entry::Entry;
match map.entry("key") {
Entry::Occupied(entry) => {
// entry.get() doesn't hold the lock!
let value = entry.get(); // Returns reference, lock released
// entry.into_ref() would hold the lock
}
Entry::Vacant(entry) => {
entry.insert("value");
}
}
// or_insert returns a reference guard
let value = map.entry("key").or_insert("default");
// This guard DOES hold the lock!
// Be careful with scope
}Understanding which operations return guards and how long locks are held is critical.
Scope Management
use dashmap::DashMap;
fn scope_management() {
let map = DashMap::new();
map.insert("key", 0);
// BAD: Lock held for entire scope
let result = {
let mut guard = map.get_mut("key").unwrap();
*guard += 1;
// Do other work while holding lock...
some_function(&map); // This might deadlock if it tries to access same key!
*guard
}; // Lock released here
// GOOD: Minimize lock scope
{
let mut guard = map.get_mut("key").unwrap();
*guard += 1;
} // Lock released immediately
some_function(&map); // Safe now
// GOOD: Use entry for atomic operations
map.entry("key").and_modify(|v| *v += 1);
// Lock released immediately after modification
// GOOD: Extract value if needed later
let value = {
let guard = map.get("key").unwrap();
*guard // Copy value
}; // Lock released
// Can use value without holding lock
println!("Value: {}", value);
}
fn some_function(map: &DashMap<&str, i32>) {
// This could deadlock if called while holding a get_mut guard
// on the same key in another thread
if let Some(v) = map.get("key") {
println!("Read: {}", v);
}
}Minimize lock scope with get_mut; use entry for atomic operations.
Real-World Example: Counter Cache
use dashmap::DashMap;
use std::sync::Arc;
struct CounterCache {
counters: DashMap<String, u64>,
}
impl CounterCache {
fn new() -> Self {
Self {
counters: DashMap::new(),
}
}
// Increment counter, returning new value
// Uses entry API for atomic check-and-insert
fn increment(&self, key: &str) -> u64 {
self.counters
.entry(key.to_string())
.and_modify(|v| *v += 1)
.or_insert(1)
.clone() // Clone to release lock
}
// Get counter value, initializing to 0 if absent
// Uses entry API
fn get_or_default(&self, key: &str) -> u64 {
*self.counters.entry(key.to_string()).or_insert(0)
}
// Reset counter to 0
// Uses get_mut for single modification
fn reset(&self, key: &str) {
if let Some(mut counter) = self.counters.get_mut(key) {
*counter = 0;
}
}
// Transfer value between counters atomically
// Requires careful lock management
fn transfer(&self, from: &str, to: &str, amount: u64) -> Result<(), String> {
// Order keys to prevent deadlock
let (first, second) = if from < to { (from, to) } else { (to, from) };
// Acquire locks in consistent order
let mut first_guard = self.counters.get_mut(first).ok_or("Source not found")?;
let mut second_guard = self.counters.get_mut(second).ok_or("Destination not found")?;
if first == from {
if *first_guard < amount {
return Err("Insufficient balance".to_string());
}
*first_guard -= amount;
*second_guard += amount;
} else {
if *second_guard < amount {
return Err("Insufficient balance".to_string());
}
*second_guard -= amount;
*first_guard += amount;
}
Ok(())
}
}Different patterns for different access patterns.
Real-World Example: Request Deduplication
use dashmap::DashMap;
use std::sync::Arc;
struct RequestDeduplicator {
pending: DashMap<String, Arc<()>>,
}
impl RequestDeduplicator {
fn new() -> Self {
Self {
pending: DashMap::new(),
}
}
// Check if request is in-flight, mark as pending if not
// Uses entry API for atomic check-and-insert
fn try_start(&self, request_id: &str) -> bool {
// Returns true if we successfully inserted (no duplicate)
// Returns false if key already exists (duplicate)
match self.pending.entry(request_id.to_string()) {
dashmap::mapref::entry::Entry::Vacant(entry) => {
entry.insert(Arc::new(()));
true // Started successfully
}
dashmap::mapref::entry::Entry::Occupied(_) => {
false // Already in progress
}
}
}
// Mark request as complete
fn finish(&self, request_id: &str) {
self.pending.remove(request_id);
}
// Check if request is pending
fn is_pending(&self, request_id: &str) -> bool {
self.pending.contains_key(request_id)
}
}entry API is perfect for atomic check-and-insert patterns.
Summary Table
use dashmap::DashMap;
fn summary() {
// | Aspect | entry API | get_mut |
// |---------------------|------------------------|-----------------------|
// | Lock duration | Minimal (one op) | Scope-based |
// | Conditional insert | Atomic (correct) | Race condition risk |
// | Multiple updates | Awkward (chained) | Natural |
// | Readability | Good for simple ops | Good for complex ops |
// | Deadlock risk | Lower (shorter locks) | Higher (scope locks) |
// | Performance | Better for single ops | Better for multi ops |
// | Use case | Check-then-insert | Modify existing |
// Use entry when:
// - Inserting if absent
// - Single atomic modification
// - Avoiding race conditions
// - Minimizing lock time
// Use get_mut when:
// - Multiple modifications needed
// - Complex conditional logic
// - Reading and writing multiple times
// - Need to hold lock for computation
}Synthesis
Quick reference:
use dashmap::DashMap;
// Conditional insertion: ALWAYS use entry
map.entry("key").or_insert("value");
map.entry("key").or_insert_with(|| compute_value());
// Multiple modifications: use get_mut
if let Some(mut v) = map.get_mut("key") {
v.field1 += 1;
v.field2 -= 1;
v.field3 = compute(v.field1);
}
// Single modification: entry is fine
map.entry("key").and_modify(|v| *v += 1);
// Check existence before insert: use entry
// WRONG:
// if !map.contains_key("key") { map.insert("key", "value"); }
// CORRECT:
map.entry("key").or_insert("value");Key insight: The fundamental difference is lock duration and atomicity. entry acquires a lock, performs exactly one operation (check-then-insert, or modify), and releases the lockāall atomically. This makes entry the correct choice for "insert if absent" patterns because there's no window between the check and the insert where another thread could race. get_mut acquires a lock and returns a guard that holds it for the entire scope, which is necessary when you need to perform multiple reads and writes on the same value without interleaving with other threadsāthink "read current balance, check overdraft, apply fee, write new balance." The trade-off is that get_mut can cause contention: other threads trying to access the same shard block for the entire scope, not just during modification. The deadlock risk comes from holding get_mut guards across multiple DashMaps or across blocking operationsāentry reduces this risk by minimizing lock duration, but neither helps if you're acquiring locks in inconsistent orders. For conditional insertion specifically, entry isn't just cleanerāit's the only correct choice, because get_mut can't atomically check-then-insert; the is_none() check and the subsequent insert() are two separate operations with a race window between them.
