How do I use DashMap for concurrent HashMaps in Rust?

Walkthrough

The dashmap crate provides a concurrent hash map implementation that allows lock-free reads and fine-grained locking for writes. Unlike wrapping a HashMap in a Mutex or RwLock, DashMap shards the map into multiple segments, each with its own lock. This allows concurrent operations on different keys to proceed in parallel, significantly improving performance in multi-threaded scenarios. It's ideal for caches, registries, and any shared mutable state accessed from multiple threads.

Key concepts:

  1. Sharding — map is divided into segments with independent locks
  2. Lock-free reads — multiple readers can access simultaneously
  3. Fine-grained locking — writes only lock the relevant segment
  4. Entry API — atomic get-and-modify operations
  5. Iterators — safe iteration with automatic segment handling

Code Example

# Cargo.toml
[dependencies]
dashmap = "5"
use dashmap::DashMap;
use std::sync::Arc;
use std::thread;
 
fn main() {
    let map = Arc::new(DashMap::new());
    
    map.insert("key", 42);
    
    if let Some(value) = map.get(&"key") {
        println!("Value: {}", *value);
    }
}

Basic Operations

use dashmap::DashMap;
 
fn main() {
    let map = DashMap::new();
    
    // Insert entries
    map.insert("apple", 1);
    map.insert("banana", 2);
    map.insert("cherry", 3);
    
    // Get values
    if let Some(value) = map.get(&"apple") {
        println!("Apple: {}", *value);
    }
    
    // Check if key exists
    println!("Contains 'banana': {}", map.contains_key(&"banana"));
    
    // Remove entries
    let removed = map.remove(&"banana");
    println!("Removed: {:?}", removed);
    
    // Length
    println!("Length: {}", map.len());
    
    // Check if empty
    println!("Is empty: {}", map.is_empty());
    
    // Clear all entries
    map.clear();
    println!("After clear: {}", map.len());
}

Creating DashMap with Options

use dashmap::DashMap;
 
fn main() {
    // Default
    let map: DashMap<i32, String> = DashMap::new();
    
    // With capacity (pre-allocate buckets)
    let map: DashMap<i32, String> = DashMap::with_capacity(1000);
    
    // With shard amount (must be power of 2)
    // More shards = more parallelism but higher memory overhead
    let map: DashMap<i32, String> = DashMap::with_shard_amount(16);
    
    // With capacity and shard amount
    let map: DashMap<i32, String> = DashMap::with_capacity_and_shard_amount(1000, 32);
    
    // Custom hasher
    use std::collections::hash_map::RandomState;
    let map: DashMap<i32, String, RandomState> = 
        DashMap::with_hasher(RandomState::new());
    
    println!("Created maps with various configurations");
}

Thread-Safe Concurrent Access

use dashmap::DashMap;
use std::sync::Arc;
use std::thread;
 
fn main() {
    let map = Arc::new(DashMap::new());
    let mut handles = vec![];
    
    // Spawn writers
    for i in 0..10 {
        let map = Arc::clone(&map);
        let handle = thread::spawn(move || {
            for j in 0..100 {
                let key = format!("thread-{}-key-{}", i, j);
                map.insert(key, i * 100 + j);
            }
        });
        handles.push(handle);
    }
    
    // Spawn readers
    for _ in 0..5 {
        let map = Arc::clone(&map);
        let handle = thread::spawn(move || {
            for i in 0..10 {
                for j in 0..100 {
                    let key = format!("thread-{}-key-{}", i, j);
                    if let Some(value) = map.get(&key) {
                        let _ = *value;
                    }
                }
            }
        });
        handles.push(handle);
    }
    
    for handle in handles {
        handle.join().unwrap();
    }
    
    println!("Final map size: {}", map.len());
}

Entry API

use dashmap::DashMap;
 
fn main() {
    let map = DashMap::new();
    
    // entry().or_insert() - insert if missing
    let entry = map.entry("count".to_string()).or_insert(0);
    println!("Count: {}", *entry);
    
    // entry().or_insert_with() - lazy initialization
    let entry = map.entry("lazy".to_string()).or_insert_with(|| {
        println!("Computing value...");
        42
    });
    println!("Lazy value: {}", *entry);
    
    // and_modify() - modify if exists
    map.entry("count".to_string())
        .and_modify(|v| *v += 1)
        .or_insert(1);
    println!("Count after increment: {}", map.get(&"count".to_string()).unwrap());
    
    // entry().or_default() - insert default if missing
    let entry = map.entry("default".to_string()).or_default();
    println!("Default: {}", *entry);
    
    // key() and value() on entry
    if let Some(entry) = map.get_key_value(&"count") {
        println!("Key: {}, Value: {}", entry.key(), entry.value());
    }
}

Mutable Access

use dashmap::DashMap;
 
fn main() {
    let map = DashMap::new();
    map.insert("counter", 0);
    
    // get_mut returns a reference that allows mutation
    if let Some(mut value) = map.get_mut(&"counter") {
        *value += 1;
    }
    
    println!("Counter: {}", map.get(&"counter").unwrap());
    
    // Mutate with closure
    map.alter(&"counter", |_, v| v * 2);
    println!("After alter: {}", map.get(&"counter").unwrap());
    
    // Atomic update
    map.update(&"counter", |_, v| v + 10);
    println!("After update: {}", map.get(&"counter").unwrap());
}

Iteration

use dashmap::DashMap;
 
fn main() {
    let map = DashMap::new();
    
    map.insert("a", 1);
    map.insert("b", 2);
    map.insert("c", 3);
    
    // Iterate over entries
    println!("Entries:");
    for entry in map.iter() {
        println!("  {} -> {}", entry.key(), entry.value());
    }
    
    // Iterate over keys
    println!("\nKeys:");
    for key in map.iter_keys() {
        println!("  {}", key);
    }
    
    // Iterate over values
    println!("\nValues:");
    for value in map.iter_values() {
        println!("  {}", value);
    }
    
    // Mutable iteration
    for mut entry in map.iter_mut() {
        *entry.value_mut() *= 2;
    }
    
    println!("\nAfter doubling:");
    for entry in map.iter() {
        println!("  {} -> {}", entry.key(), entry.value());
    }
}

Entry Blocking and Locking

use dashmap::DashMap;
 
fn main() {
    let map = DashMap::new();
    map.insert("key", "value");
    
    // get() returns a reference guard
    // The entry is read-locked while the guard exists
    {
        let _guard = map.get(&"key").unwrap();
        println!("Holding read lock on 'key'");
        // Other reads can proceed, writes will block
    }
    println!("Read lock released");
    
    // get_mut() returns a write guard
    {
        let mut guard = map.get_mut(&"key").unwrap();
        *guard = "modified";
        println!("Holding write lock on 'key'");
    }
    println!("Write lock released");
    
    // Check if entry is locked (for advanced use cases)
    println!("Is 'key' locked: {}", map.is_locked());
}

Shard Access

use dashmap::DashMap;
 
fn main() {
    let map: DashMap<i32, String> = DashMap::with_shard_amount(16);
    
    // Get the number of shards
    println!("Shards: {}", map.shards().len());
    
    // Determine which shard a key belongs to
    let key = 42;
    let shard_index = map.determine_map(&key).into_usize();
    println!("Key {} is in shard {}", key, shard_index);
    
    // Get a specific shard (advanced use)
    // Note: This locks the shard
    
    // Capacity per shard
    println!("Capacity: {}", map.capacity());
}

Atomic Operations

use dashmap::DashMap;
 
fn main() {
    let map = DashMap::new();
    
    // insert() returns old value if present
    let old = map.insert("key", 1);
    println!("Old value: {:?}", old); // None
    
    let old = map.insert("key", 2);
    println!("Old value: {:?}", old); // Some(1)
    
    // remove() returns the removed entry
    let removed = map.remove(&"key");
    println!("Removed: {:?}", removed);
    
    // swap() - insert and return old value atomically
    map.insert("a", 1);
    let swapped = map.swap("a", 10);
    println!("Swapped: {:?}", swapped);
    println!("New value: {:?}", map.get(&"a"));
}

Conditional Operations

use dashmap::DashMap;
 
fn main() {
    let map = DashMap::new();
    
    // insert_if() - insert only if condition is met
    map.insert("key", 1);
    
    // Only insert if current value < 5
    map.insert_if("key", 10, |_, current| *current < 5);
    println!("After insert_if (< 5): {:?}", map.get(&"key"));
    
    // Only insert if current value < 0 (false)
    map.insert_if("key", 20, |_, current| *current < 0);
    println!("After insert_if (< 0): {:?}", map.get(&"key"));
    
    // remove_if() - remove only if condition is met
    let removed = map.remove_if(&"key", |_, v| *v > 5);
    println!("Removed (> 5): {:?}", removed);
}

Computed Values

use dashmap::DashMap;
 
fn main() {
    let map = DashMap::new();
    
    // get_or_insert() - get or insert default
    let value = map.get_or_insert("count", 0);
    println!("Count: {}", *value);
    
    // get_or_insert_with() - get or compute
    let value = map.get_or_insert_with("computed", || {
        println!("Computing...");
        42
    });
    println!("Computed: {}", *value);
    
    // Second call doesn't recompute
    let value = map.get_or_insert_with("computed", || {
        panic!("Should not be called");
    });
    println!("Cached: {}", *value);
}

Retain and Filter

use dashmap::DashMap;
 
fn main() {
    let map = DashMap::new();
    
    for i in 0..10 {
        map.insert(i, i * 10);
    }
    
    println!("Before retain: {} entries", map.len());
    
    // Retain only even keys
    map.retain(|key, value| {
        println!("Checking {} -> {}", key, value);
        key % 2 == 0
    });
    
    println!("\nAfter retain (even keys):");
    for entry in map.iter() {
        println!("  {} -> {}", entry.key(), entry.value());
    }
}

HashMap-like Conversions

use dashmap::DashMap;
use std::collections::HashMap;
 
fn main() {
    let mut hash_map = HashMap::new();
    hash_map.insert("a", 1);
    hash_map.insert("b", 2);
    hash_map.insert("c", 3);
    
    // From HashMap to DashMap
    let dash_map: DashMap<&str, i32> = DashMap::from_iter(hash_map.iter().map(|(k, v)| (*k, *v)));
    println!("DashMap size: {}", dash_map.len());
    
    // From DashMap to HashMap
    let back_to_hash_map: HashMap<&str, i32> = dash_map.iter()
        .map(|entry| (entry.key().clone(), *entry.value()))
        .collect();
    
    println!("HashMap size: {}", back_to_hash_map.len());
    
    // Clone DashMap
    let cloned = dash_map.clone();
    println!("Cloned size: {}", cloned.len());
}

Real-World: In-Memory Cache

use dashmap::DashMap;
use std::sync::Arc;
use std::time::{Duration, Instant};
 
#[derive(Clone)]
struct CacheEntry<T> {
    value: T,
    expires_at: Instant,
}
 
pub struct Cache<K, V> {
    map: DashMap<K, CacheEntry<V>>,
    default_ttl: Duration,
}
 
impl<K: std::hash::Hash + Eq + Clone, V: Clone> Cache<K, V> {
    pub fn new(default_ttl: Duration) -> Self {
        Self {
            map: DashMap::new(),
            default_ttl,
        }
    }
    
    pub fn get(&self, key: &K) -> Option<V> {
        self.map.get(key).and_then(|entry| {
            if entry.expires_at > Instant::now() {
                Some(entry.value.clone())
            } else {
                None
            }
        })
    }
    
    pub fn set(&self, key: K, value: V) {
        self.set_with_ttl(key, value, self.default_ttl);
    }
    
    pub fn set_with_ttl(&self, key: K, value: V, ttl: Duration) {
        let entry = CacheEntry {
            value,
            expires_at: Instant::now() + ttl,
        };
        self.map.insert(key, entry);
    }
    
    pub fn delete(&self, key: &K) {
        self.map.remove(key);
    }
    
    pub fn cleanup_expired(&self) {
        let now = Instant::now();
        self.map.retain(|_, entry| entry.expires_at > now);
    }
}
 
fn main() {
    let cache = Arc::new(Cache::new(Duration::from_secs(60)));
    
    cache.set("user:1", "Alice".to_string());
    cache.set("user:2", "Bob".to_string());
    
    if let Some(name) = cache.get(&"user:1") {
        println!("Found user: {}", name);
    }
    
    // Concurrent access
    let cache_reader = Arc::clone(&cache);
    let handle = std::thread::spawn(move || {
        if let Some(name) = cache_reader.get(&"user:2") {
            println!("Thread found: {}", name);
        }
    });
    
    handle.join().unwrap();
}

Real-World: Rate Limiter

use dashmap::DashMap;
use std::time::{Duration, Instant};
 
struct RateLimitEntry {
    count: u32,
    window_start: Instant,
}
 
pub struct RateLimiter {
    entries: DashMap<String, RateLimitEntry>,
    max_requests: u32,
    window: Duration,
}
 
impl RateLimiter {
    pub fn new(max_requests: u32, window: Duration) -> Self {
        Self {
            entries: DashMap::new(),
            max_requests,
            window,
        }
    }
    
    pub fn check(&self, client_id: &str) -> bool {
        let now = Instant::now();
        
        let mut entry = self.entries.entry(client_id.to_string())
            .or_insert(RateLimitEntry {
                count: 0,
                window_start: now,
            });
        
        // Check if window expired
        if now.duration_since(entry.window_start) > self.window {
            entry.count = 0;
            entry.window_start = now;
        }
        
        entry.count += 1;
        entry.count <= self.max_requests
    }
    
    pub fn remaining(&self, client_id: &str) -> u32 {
        if let Some(entry) = self.entries.get(client_id) {
            if Instant::now().duration_since(entry.window_start) <= self.window {
                return self.max_requests.saturating_sub(entry.count);
            }
        }
        self.max_requests
    }
    
    pub fn cleanup(&self) {
        let now = Instant::now();
        self.entries.retain(|_, entry| {
            now.duration_since(entry.window_start) <= self.window * 2
        });
    }
}
 
fn main() {
    let limiter = RateLimiter::new(5, Duration::from_secs(60));
    
    for i in 0..7 {
        let allowed = limiter.check("client_123");
        let remaining = limiter.remaining("client_123");
        println!("Request {}: allowed={}, remaining={}", i + 1, allowed, remaining);
    }
}

Real-World: Pub/Sub System

use dashmap::DashMap;
use std::sync::Arc;
 
pub struct PubSub<T> {
    subscribers: DashMap<String, Vec<Box<dyn Fn(&T) + Send + Sync>>>,
}
 
impl<T> PubSub<T> {
    pub fn new() -> Self {
        Self {
            subscribers: DashMap::new(),
        }
    }
    
    pub fn subscribe<F>(&self, topic: &str, callback: F)
    where
        F: Fn(&T) + Send + Sync + 'static,
    {
        self.subscribers
            .entry(topic.to_string())
            .or_default()
            .push(Box::new(callback));
    }
    
    pub fn publish(&self, topic: &str, message: T) {
        if let Some(callbacks) = self.subscribers.get(topic) {
            for callback in callbacks.iter() {
                callback(&message);
            }
        }
    }
    
    pub fn subscriber_count(&self, topic: &str) -> usize {
        self.subscribers.get(topic).map(|c| c.len()).unwrap_or(0)
    }
}
 
fn main() {
    let pubsub = Arc::new(PubSub::new());
    
    // Subscribe
    pubsub.subscribe("news", |msg: &String| {
        println!("News subscriber 1: {}", msg);
    });
    
    pubsub.subscribe("news", |msg: &String| {
        println!("News subscriber 2: {}", msg);
    });
    
    println!("Subscribers: {}", pubsub.subscriber_count("news"));
    
    // Publish
    pubsub.publish("news", "Hello World!".to_string());
}

Real-World: Connection Pool

use dashmap::DashMap;
use std::sync::Arc;
use std::time::{Duration, Instant};
 
#[derive(Debug)]
struct Connection {
    id: u64,
    created_at: Instant,
}
 
impl Connection {
    fn new(id: u64) -> Self {
        Self {
            id,
            created_at: Instant::now(),
        }
    }
    
    fn is_valid(&self) -> bool {
        self.created_at.elapsed() < Duration::from_secs(300)
    }
}
 
pub struct ConnectionPool {
    connections: DashMap<String, Connection>,
    next_id: std::sync::atomic::AtomicU64,
}
 
impl ConnectionPool {
    pub fn new() -> Self {
        Self {
            connections: DashMap::new(),
            max_size,
            next_id: std::sync::atomic::AtomicU64::new(1),
        }
    }
    
    pub fn get_or_create(&self, name: &str) -> Arc<Connection> {
        // Check existing connection
        if let Some(conn) = self.connections.get(name) {
            if conn.is_valid() {
                return Arc::new(Connection::new(conn.id));
            }
        }
        
        // Create new connection
        let id = self.next_id.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
        let conn = Connection::new(id);
        self.connections.insert(name.to_string(), conn.clone());
        
        Arc::new(conn)
    }
    
    pub fn remove(&self, name: &str) {
        self.connections.remove(name);
    }
    
    pub fn len(&self) -> usize {
        self.connections.len()
    }
    
    pub fn cleanup_invalid(&self) {
        self.connections.retain(|_, conn| conn.is_valid());
    }
}
 
fn main() {
    let pool = ConnectionPool::new();
    
    let conn1 = pool.get_or_create("db1");
    println!("Connection: {:?}", *conn1);
    
    let conn2 = pool.get_or_create("db1"); // Same connection
    println!("Pooled connection: {:?}", *conn2);
    
    println!("Pool size: {}", pool.len());
}

Comparing with RwLock

use dashmap::DashMap;
use std::collections::HashMap;
use std::sync::{Arc, RwLock};
use std::time::Instant;
 
fn benchmark_dashmap(count: usize) -> Duration {
    let map = Arc::new(DashMap::new());
    
    let start = Instant::now();
    
    let handles: Vec<_> = (0..4)
        .map(|t| {
            let map = Arc::clone(&map);
            std::thread::spawn(move || {
                for i in 0..count {
                    let key = t * count + i;
                    map.insert(key, i);
                    let _ = map.get(&key);
                }
            })
        })
        .collect();
    
    for handle in handles {
        handle.join().unwrap();
    }
    
    start.elapsed()
}
 
fn benchmark_rwlock(count: usize) -> Duration {
    let map = Arc::new(RwLock::new(HashMap::new()));
    
    let start = Instant::now();
    
    let handles: Vec<_> = (0..4)
        .map(|t| {
            let map = Arc::clone(&map);
            std::thread::spawn(move || {
                for i in 0..count {
                    let key = t * count + i;
                    map.write().unwrap().insert(key, i);
                    let _ = map.read().unwrap().get(&key);
                }
            })
        })
        .collect();
    
    for handle in handles {
        handle.join().unwrap();
    }
    
    start.elapsed()
}
 
fn main() {
    let count = 10000;
    
    let dashmap_time = benchmark_dashmap(count);
    let rwlock_time = benchmark_rwlock(count);
    
    println!("DashMap: {:?}", dashmap_time);
    println!("RwLock<HashMap>: {:?}", rwlock_time);
    println!("Speedup: {:.2}x", rwlock_time.as_secs_f64() / dashmap_time.as_secs_f64());
}

Handling Complex Values

use dashmap::DashMap;
use std::collections::HashSet;
 
fn main() {
    let map: DashMap<String, HashSet<i32>> = DashMap::new();
    
    // Add to nested collection
    map.entry("set_a".to_string())
        .or_insert_with(HashSet::new)
        .insert(1);
    
    // Modify nested collection
    if let Some(mut set) = map.get_mut(&"set_a") {
        set.insert(2);
        set.insert(3);
    }
    
    // Read nested collection
    if let Some(set) = map.get(&"set_a") {
        println!("Set contents: {:?}", *set);
    }
}

Summary

  • DashMap::new() creates a concurrent hash map
  • map.insert(key, value) adds entries atomically
  • map.get(&key) returns a read guard (lock-free)
  • map.get_mut(&key) returns a write guard
  • map.entry(key) provides entry API for conditional operations
  • Sharding enables fine-grained locking for better parallelism
  • map.iter() and map.iter_mut() for safe iteration
  • map.retain() for filtering entries
  • Wrap in Arc for sharing across threads
  • Much faster than Mutex<HashMap> for concurrent workloads
  • Perfect for: caches, registries, connection pools, rate limiters, pub/sub systems