How do I share a hash map across threads safely?

Walkthrough

DashMap is a concurrent hash map that provides thread-safe access without requiring external locking. It uses sharding—dividing data into multiple segments each with its own lock—allowing concurrent operations on different keys with minimal contention. DashMap offers a HashMap-like API while being safe to share across threads.

Key features:

  1. Sharded locking — multiple shards reduce lock contention
  2. Familiar API — similar to std::collections::HashMap
  3. Fine-grained locking — only locks the relevant shard
  4. Atomic operations — entry API for conditional inserts/updates
  5. Iterators — safe iteration with automatic guard handling

DashMap is ideal for caches, registries, and shared state in concurrent applications.

Code Example

# Cargo.toml
[dependencies]
dashmap = "5"
use dashmap::DashMap;
use std::sync::Arc;
use std::thread;
 
fn main() {
    // Create a DashMap (internally sharded)
    let map: DashMap<String, u32> = DashMap::new();
    
    // Basic operations work like HashMap
    map.insert("apple".to_string(), 1);
    map.insert("banana".to_string(), 2);
    
    // Read access
    if let Some(entry) = map.get("apple") {
        println!("apple: {}", *entry);
    }
    
    // Update existing value
    if let Some(mut entry) = map.get_mut("banana") {
        *entry += 10;
    }
    
    // Remove
    map.remove("apple");
    
    // Check existence
    println!("Contains banana: {}", map.contains_key("banana"));
    
    // Iterate
    for entry in map.iter() {
        println!("{}: {}", entry.key(), entry.value());
    }
    
    println!("Length: {}", map.len());
}

Concurrent Access Across Threads

use dashmap::DashMap;
use std::sync::Arc;
use std::thread;
 
fn main() {
    // Wrap in Arc for sharing across threads
    let map = Arc::new(DashMap::<String, i32>::new());
    let mut handles = vec![];
    
    // Spawn multiple writer threads
    for i in 0..10 {
        let map_clone = Arc::clone(&map);
        let handle = thread::spawn(move || {
            for j in 0..100 {
                let key = format!("thread-{}-key-{}", i, j);
                map_clone.insert(key, i * 1000 + j);
            }
        });
        handles.push(handle);
    }
    
    // Spawn reader threads concurrently
    for _ in 0..5 {
        let map_clone = Arc::clone(&map);
        let handle = thread::spawn(move || {
            for i in 0..100 {
                let key = format!("thread-0-key-{}", i);
                if let Some(value) = map_clone.get(&key) {
                    println!("Read: {} = {}", key, *value);
                }
            }
        });
        handles.push(handle);
    }
    
    for handle in handles {
        handle.join().unwrap();
    }
    
    println!("Total entries: {}", map.len());
}

Entry API for Atomic Operations

use dashmap::DashMap;
 
fn main() {
    let map: DashMap<String, u32> = DashMap::new();
    
    // or_insert: insert if missing
    map.entry("counter".to_string()).or_insert(0);
    
    // or_insert_with: lazy insertion
    map.entry("lazy".to_string()).or_insert_with(|| {
        println!("Computing value...");
        42
    });
    
    // and_modify: update existing value
    map.entry("counter".to_string())
        .and_modify(|v| *v += 1);
    
    // Combined: update if exists, insert if not
    map.entry("counter".to_string())
        .and_modify(|v| *v += 1)
        .or_insert(1);
    
    println!("Counter: {}", map.get("counter").unwrap());
    
    // Entry key access
    for entry in map.iter() {
        println!("{}: {}", entry.key(), entry.value());
    }
}

Cache Implementation Pattern

use dashmap::DashMap;
use std::sync::Arc;
use std::time::{Duration, Instant};
use std::thread;
 
#[derive(Clone)]
struct CacheEntry {
    value: String,
    expires_at: Instant,
}
 
struct Cache {
    data: DashMap<String, CacheEntry>,
}
 
impl Cache {
    fn new() -> Self {
        Cache {
            data: DashMap::new(),
        }
    }
    
    fn get(&self, key: &str) -> Option<String> {
        self.data.get(key).and_then(|entry| {
            if entry.expires_at > Instant::now() {
                Some(entry.value.clone())
            } else {
                None // Expired
            }
        })
    }
    
    fn set(&self, key: String, value: String, ttl: Duration) {
        let entry = CacheEntry {
            value,
            expires_at: Instant::now() + ttl,
        };
        self.data.insert(key, entry);
    }
    
    fn remove(&self, key: &str) {
        self.data.remove(key);
    }
    
    fn clear_expired(&self) {
        let now = Instant::now();
        self.data.retain(|_, entry| entry.expires_at > now);
    }
    
    fn len(&self) -> usize {
        self.data.len()
    }
}
 
fn main() {
    let cache = Arc::new(Cache::new());
    
    // Set some values
    cache.set("user:1".to_string(), "Alice".to_string(), Duration::from_secs(5));
    cache.set("user:2".to_string(), "Bob".to_string(), Duration::from_millis(100));
    
    // Retrieve values
    println!("user:1 = {:?}", cache.get("user:1"));
    println!("user:2 = {:?}", cache.get("user:2"));
    
    thread::sleep(Duration::from_millis(150));
    
    // user:2 should be expired
    println!("After 150ms - user:2 = {:?}", cache.get("user:2"));
    println!("After 150ms - user:1 = {:?}", cache.get("user:1"));
    
    // Clean up expired entries
    cache.clear_expired();
    println!("Cache size after cleanup: {}", cache.len());
}

Read-Only Views and Shards

use dashmap::DashMap;
 
fn main() {
    let map: DashMap<i32, String> = DashMap::new();
    
    for i in 0..20 {
        map.insert(i, format!("value-{}", i));
    }
    
    // Iterate over shards
    println!("Number of shards: {}", map.shards().len());
    
    // Access individual shard (advanced use case)
    for (idx, shard) in map.shards().iter().enumerate() {
        let guard = shard.read();
        println!("Shard {}: {} entries", idx, guard.len());
    }
    
    // Iterate over all entries
    let mut keys = Vec::new();
    for entry in map.iter() {
        keys.push(*entry.key());
    }
    keys.sort();
    println!("Keys: {:?}", keys);
    
    // Filter and retain
    map.retain(|k, _| *k % 2 == 0);
    println!("After retaining even keys: {} entries", map.len());
}

Concurrent Counter Pattern

use dashmap::DashMap;
use std::sync::Arc;
use std::thread;
 
fn main() {
    let counters = Arc::new(DashMap::<String, u64>::new());
    let mut handles = vec![];
    
    // Multiple threads incrementing counters
    for i in 0..5 {
        let counters_clone = Arc::clone(&counters);
        let handle = thread::spawn(move || {
            for _ in 0..1000 {
                // Atomically increment counter
                counters_clone
                    .entry(format!("counter-{}", i % 3))
                    .and_modify(|v| *v += 1)
                    .or_insert(1);
            }
        });
        handles.push(handle);
    }
    
    for handle in handles {
        handle.join().unwrap();
    }
    
    for entry in counters.iter() {
        println!("{}: {}", entry.key(), entry.value());
    }
}

Comparison with Standard RwLock

use dashmap::DashMap;
use std::collections::HashMap;
use std::sync::{Arc, RwLock};
use std::thread;
use std::time::Instant;
 
fn main() {
    let iterations = 100_000;
    
    // DashMap approach
    {
        let map = Arc::new(DashMap::<u32, u32>::new());
        let start = Instant::now();
        
        let mut handles = vec![];
        for _ in 0..4 {
            let map_clone = Arc::clone(&map);
            handles.push(thread::spawn(move || {
                for i in 0..iterations {
                    map_clone.insert(i % 1000, i);
                }
            }));
        }
        
        for h in handles {
            h.join().unwrap();
        }
        
        println!("DashMap: {:?}", start.elapsed());
    }
    
    // RwLock<HashMap> approach
    {
        let map = Arc::new(RwLock::new(HashMap::<u32, u32>::new()));
        let start = Instant::now();
        
        let mut handles = vec![];
        for _ in 0..4 {
            let map_clone = Arc::clone(&map);
            handles.push(thread::spawn(move || {
                for i in 0..iterations {
                    let mut guard = map_clone.write().unwrap();
                    guard.insert(i % 1000, i);
                }
            }));
        }
        
        for h in handles {
            h.join().unwrap();
        }
        
        println!("RwLock<HashMap>: {:?}", start.elapsed());
    }
}

Custom Types as Keys

use dashmap::DashMap;
use std::hash::{Hash, Hasher};
 
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
struct UserId {
    id: u64,
    region: String,
}
 
#[derive(Debug, Clone)]
struct User {
    name: String,
    email: String,
    active: bool,
}
 
fn main() {
    let users: DashMap<UserId, User> = DashMap::new();
    
    let id1 = UserId { id: 1, region: "us".to_string() };
    let id2 = UserId { id: 2, region: "eu".to_string() };
    
    users.insert(id1.clone(), User {
        name: "Alice".to_string(),
        email: "alice@example.com".to_string(),
        active: true,
    });
    
    users.insert(id2.clone(), User {
        name: "Bob".to_string(),
        email: "bob@example.com".to_string(),
        active: false,
    });
    
    // Lookup by key
    if let Some(user) = users.get(&id1) {
        println!("Found: {} ({})", user.name, user.email);
    }
    
    // Update
    users.entry(id2.clone()).and_modify(|u| u.active = true);
    
    // List all users
    for entry in users.iter() {
        println!("{:?}: {:?}", entry.key(), entry.value());
    }
}

Summary

  • Use DashMap as a drop-in concurrent replacement for HashMap
  • Wrap in Arc when sharing across threads: Arc::new(DashMap::new())
  • Basic operations: insert(), get(), get_mut(), remove(), contains_key()
  • Use entry() API for atomic conditional operations like counters and caches
  • or_insert() and or_insert_with() insert if key is missing
  • and_modify() updates existing values; combine with or_insert() for upserts
  • Iterate safely with iter() — returns guarded references
  • retain() filters entries in place; clear() removes all
  • Access shards() for advanced introspection of internal segments
  • DashMap typically outperforms RwLock<HashMap> under contention due to sharding
  • Keys must implement Hash + Eq; values can be any Sized type
  • Use for caches, registries, rate limiters, and any shared mutable state