How do I use a concurrent HashMap with DashMap in Rust?

Walkthrough

The dashmap crate provides a blazing fast, thread-safe HashMap implementation in Rust. Unlike std::collections::HashMap wrapped in a Mutex or RwLock, DashMap uses sharded locking (also known as striped locking) to allow concurrent access to different parts of the map simultaneously. This dramatically reduces contention in multi-threaded scenarios. Each shard has its own lock, so operations on different keys often proceed in parallel.

Key features:

  1. Sharded design — internal array of independent hash maps with separate locks
  2. Concurrent access — multiple readers and writers can work simultaneously
  3. Familiar API — similar to std::collections::HashMap with additional methods
  4. Entry API — atomic update patterns like entry().or_insert()
  5. Zero-cost reads — read operations don't block each other

Code Example

# Cargo.toml
[dependencies]
dashmap = "5.5"
use dashmap::DashMap;
use std::sync::Arc;
use std::thread;
 
fn main() {
    let map: Arc<DashMap<i32, String>> = Arc::new(DashMap::new());
    
    let mut handles = vec![];
    
    for i in 0..10 {
        let map = Arc::clone(&map);
        handles.push(thread::spawn(move || {
            map.insert(i, format!("value-{}", i));
        }));
    }
    
    for handle in handles {
        handle.join().unwrap();
    }
    
    println!("Map has {} entries", map.len());
    
    if let Some(value) = map.get(&5) {
        println!("Key 5: {}", value);
    }
}

Basic Operations

use dashmap::DashMap;
 
fn main() {
    let map: DashMap<&str, i32> = DashMap::new();
    
    // Insert values
    map.insert("one", 1);
    map.insert("two", 2);
    map.insert("three", 3);
    
    println!("Map: {:?}", map);
    println!("Length: {}", map.len());
    println!("Is empty: {}", map.is_empty());
    
    // Get values (returns DashMapRef with RAII guard)
    if let Some(value) = map.get("one") {
        println!("Value for 'one': {}", *value);
    }
    
    // Check if contains key
    println!("Contains 'two': {}", map.contains_key("two"));
    println!("Contains 'four': {}", map.contains_key("four"));
    
    // Remove entry
    let removed = map.remove("two");
    println!("Removed: {:?}", removed);
    println!("Length after remove: {}", map.len());
    
    // Clear map
    map.clear();
    println!("Is empty after clear: {}", map.is_empty());
}

Entry API

use dashmap::DashMap;
 
fn main() {
    let map: DashMap<&str, i32> = DashMap::new();
    
    // or_insert - insert if missing
    map.entry("count").or_insert(0);
    println!("After or_insert: {:?}", map.get("count"));
    
    // or_insert_with - insert with closure if missing
    map.entry("computed").or_insert_with(|| {
        println!("Computing value...");
        42
    });
    println!("Computed: {:?}", map.get("computed"));
    
    // and_modify - modify existing value
    map.entry("count").and_modify(|v| *v += 1);
    println!("After and_modify: {:?}", map.get("count"));
    
    // or_insert_with handles missing key
    map.entry("missing").and_modify(|v| *v += 1).or_insert(100);
    println!("Missing key: {:?}", map.get("missing"));
    
    // Counter pattern
    let words = ["apple", "banana", "apple", "cherry", "banana", "apple"];
    for word in words {
        map.entry(word).and_modify(|v| *v += 1).or_insert(1);
    }
    
    println!("Word counts:");
    for entry in map.iter() {
        println!("  {} => {}", entry.key(), entry.value());
    }
}

Iteration

use dashmap::DashMap;
 
fn main() {
    let map: DashMap<i32, &str> = DashMap::new();
    
    map.insert(1, "one");
    map.insert(2, "two");
    map.insert(3, "three");
    map.insert(4, "four");
    map.insert(5, "five");
    
    // Iterate over entries (holds shard lock briefly)
    println!("Iterating:");
    for entry in map.iter() {
        println!("  {} => {}", entry.key(), entry.value());
    }
    
    // Iterate over keys
    println!("\nKeys:");
    for key in map.iter().map(|e| e.key()) {
        println!("  {}", key);
    }
    
    // Iterate over values
    println!("\nValues:");
    for value in map.iter().map(|e| e.value()) {
        println!("  {}", value);
    }
    
    // Mutable iteration
    println!("\nMutable iteration (doubling values):");
    for mut entry in map.iter_mut() {
        *entry.value_mut() = "modified";
    }
    
    for entry in map.iter() {
        println!("  {} => {}", entry.key(), entry.value());
    }
}

Updating Values

use dashmap::DashMap;
 
fn main() {
    let map: DashMap<&str, i32> = DashMap::new();
    
    map.insert("counter", 0);
    
    // Method 1: get + update (requires explicit handling)
    // Note: get() returns a reference guard
    {
        let value = map.get("counter").unwrap();
        println!("Current value: {}", *value);
    } // Guard dropped here
    
    // Method 2: entry + and_modify
    for _ in 0..5 {
        map.entry("counter").and_modify(|v| *v += 1);
    }
    println!("After 5 increments: {:?}", map.get("counter"));
    
    // Method 3: iter_mut for bulk updates
    map.insert("a", 10);
    map.insert("b", 20);
    map.insert("c", 30);
    
    for mut entry in map.iter_mut() {
        *entry.value_mut() *= 2;
    }
    
    println!("After doubling:");
    for entry in map.iter() {
        println!("  {} => {}", entry.key(), entry.value());
    }
}

Multi-threaded Access

use dashmap::DashMap;
use std::sync::Arc;
use std::thread;
 
fn main() {
    let map: Arc<DashMap<i32, i32>> = Arc::new(DashMap::new());
    
    // Initialize with values
    for i in 0..100 {
        map.insert(i, 0);
    }
    
    let mut handles = vec![];
    
    // Spawn writers
    for _ in 0..4 {
        let map = Arc::clone(&map);
        handles.push(thread::spawn(move || {
            for i in 0..100 {
                map.entry(i).and_modify(|v| *v += 1);
            }
        }));
    }
    
    // Spawn readers
    for _ in 0..2 {
        let map = Arc::clone(&map);
        handles.push(thread::spawn(move || {
            for i in 0..100 {
                if let Some(value) = map.get(&i) {
                    // Just read
                    let _ = *value;
                }
            }
        }));
    }
    
    for handle in handles {
        handle.join().unwrap();
    }
    
    // Verify all counters reached 4
    println!("Final values:");
    for i in 0..5 {
        println!("  key {} => {}", i, *map.get(&i).unwrap());
    }
    println!("All values are 4: {}", map.iter().all(|e| *e.value() == 4));
}

Sharding Configuration

use dashmap::DashMap;
 
fn main() {
    // Default: number of shards = number of CPU cores * 4
    let default_map: DashMap<i32, i32> = DashMap::new();
    println!("Default shards: {}", default_map.shards().len());
    
    // Custom number of shards (should be power of 2)
    let custom_map: DashMap<i32, i32> = DashMap::with_shard_amount(16);
    println!("Custom shards: {}", custom_map.shards().len());
    
    // With capacity and shards
    let map: DashMap<i32, i32> = DashMap::with_capacity_and_shard_amount(1000, 32);
    println!("Capacity map shards: {}", map.shards().len());
    
    // More shards = less contention but more memory overhead
    // Fewer shards = more contention but less memory overhead
    // Rule of thumb: shards ~= expected concurrent writers
}

Working with Complex Types

use dashmap::DashMap;
use std::sync::Arc;
 
#[derive(Debug, Clone)]
struct User {
    id: u32,
    name: String,
    email: String,
    active: bool,
}
 
fn main() {
    let users: DashMap<u32, User> = DashMap::new();
    
    // Insert users
    users.insert(1, User {
        id: 1,
        name: "Alice".to_string(),
        email: "alice@example.com".to_string(),
        active: true,
    });
    
    users.insert(2, User {
        id: 2,
        name: "Bob".to_string(),
        email: "bob@example.com".to_string(),
        active: false,
    });
    
    // Get user
    if let Some(user) = users.get(&1) {
        println!("User: {} ({})", user.name, user.email);
    }
    
    // Update user using entry API
    users.entry(2).and_modify(|u| u.active = true);
    
    // Get mutable access
    if let Some(mut user) = users.get_mut(&2) {
        user.email = "bob_new@example.com".to_string();
    }
    
    // Iterate
    for entry in users.iter() {
        let user = entry.value();
        println!("User {}: {} - active: {}", user.id, user.name, user.active);
    }
    
    // Filter and collect
    let active_users: Vec<_> = users.iter()
        .filter(|e| e.value().active)
        .map(|e| e.value().clone())
        .collect();
    
    println!("Active users: {}", active_users.len());
}

Atomic Operations

use dashmap::DashMap;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use std::thread;
 
fn main() {
    let map: Arc<DashMap<&str, AtomicUsize>> = Arc::new(DashMap::new());
    
    // Initialize counters
    map.insert("requests", AtomicUsize::new(0));
    map.insert("errors", AtomicUsize::new(0));
    
    let mut handles = vec![];
    
    for _ in 0..10 {
        let map = Arc::clone(&map);
        handles.push(thread::spawn(move || {
            for _ in 0..1000 {
                map.get("requests").unwrap().fetch_add(1, Ordering::SeqCst);
            }
        }));
    }
    
    for handle in handles {
        handle.join().unwrap();
    }
    
    println!("Total requests: {}", map.get("requests").unwrap().load(Ordering::SeqCst));
}

View and Shards

use dashmap::DashMap;
 
fn main() {
    let map: DashMap<i32, &str> = DashMap::new();
    
    for i in 0..10 {
        map.insert(i, &format!("value-{}", i));
    }
    
    // View individual shards
    println!("Number of shards: {}", map.shards().len());
    
    for (idx, shard) in map.shards().iter().enumerate() {
        let guard = shard.read();
        let len = guard.len();
        if len > 0 {
            println!("Shard {}: {} entries", idx, len);
        }
    }
    
    // Get shard for a specific key
    let shard_idx = map.determine_map(&5);
    println!("Key 5 is in shard {}", shard_idx);
    
    // View allows custom operations on shards
    map.view(|key, value| {
        if *key % 2 == 0 {
            println!("Even key {} => {}", key, value);
        }
    });
}

Custom Key Types

use dashmap::DashMap;
use std::hash::Hash;
 
#[derive(Debug, Clone, Hash, Eq, PartialEq)]
struct CacheKey {
    region: String,
    resource_type: String,
    id: u64,
}
 
impl CacheKey {
    fn new(region: &str, resource_type: &str, id: u64) -> Self {
        Self {
            region: region.to_string(),
            resource_type: resource_type.to_string(),
            id,
        }
    }
}
 
#[derive(Debug, Clone)]
struct CacheEntry {
    data: String,
    created_at: u64,
    ttl_seconds: u64,
}
 
impl CacheEntry {
    fn new(data: String, ttl_seconds: u64, now: u64) -> Self {
        Self {
            data,
            created_at: now,
            ttl_seconds,
        }
    }
    
    fn is_expired(&self, now: u64) -> bool {
        now > self.created_at + self.ttl_seconds
    }
}
 
fn main() {
    let cache: DashMap<CacheKey, CacheEntry> = DashMap::new();
    
    // Insert entries
    cache.insert(
        CacheKey::new("us-east-1", "user", 1),
        CacheEntry::new("user_data_1".to_string(), 3600, 0),
    );
    
    cache.insert(
        CacheKey::new("us-west-2", "product", 42),
        CacheEntry::new("product_data_42".to_string(), 1800, 0),
    );
    
    // Look up by key
    let key = CacheKey::new("us-east-1", "user", 1);
    if let Some(entry) = cache.get(&key) {
        println!("Found: {}", entry.data);
    }
    
    // Check existence
    let other_key = CacheKey::new("eu-west-1", "user", 1);
    println!("Exists: {}", cache.contains_key(&other_key));
}

Real-World Example: Rate Limiter

use dashmap::DashMap;
use std::sync::Arc;
use std::time::{Duration, Instant};
 
#[derive(Clone)]
struct RateLimitEntry {
    count: u32,
    window_start: Instant,
}
 
struct RateLimiter {
    limits: DashMap<String, RateLimitEntry>,
    max_requests: u32,
    window_duration: Duration,
}
 
impl RateLimiter {
    fn new(max_requests: u32, window_duration: Duration) -> Self {
        Self {
            limits: DashMap::new(),
            max_requests,
            window_duration,
        }
    }
    
    fn check_rate_limit(&self, client_id: &str) -> bool {
        let now = Instant::now();
        
        // Try to get existing entry
        if let Some(mut entry) = self.limits.get_mut(client_id) {
            let elapsed = now.duration_since(entry.window_start);
            
            if elapsed > self.window_duration {
                // Reset window
                entry.count = 1;
                entry.window_start = now;
                true
            } else if entry.count < self.max_requests {
                entry.count += 1;
                true
            } else {
                false
            }
        } else {
            // New entry
            self.limits.insert(client_id.to_string(), RateLimitEntry {
                count: 1,
                window_start: now,
            });
            true
        }
    }
    
    fn cleanup_expired(&self) {
        let now = Instant::now();
        let expired: Vec<_> = self.limits.iter()
            .filter(|e| now.duration_since(e.value().window_start) > self.window_duration)
            .map(|e| e.key().clone())
            .collect();
        
        for key in expired {
            self.limits.remove(&key);
        }
    }
    
    fn active_clients(&self) -> usize {
        self.limits.len()
    }
}
 
fn main() {
    let limiter = Arc::new(RateLimiter::new(5, Duration::from_secs(60)));
    
    // Simulate requests
    for i in 0..10 {
        let allowed = limiter.check_rate_limit("client-1");
        println!("Request {}: allowed = {}", i + 1, allowed);
    }
    
    // Different client
    println!("\nClient 2:");
    for i in 0..3 {
        let allowed = limiter.check_rate_limit("client-2");
        println!("Request {}: allowed = {}", i + 1, allowed);
    }
    
    println!("\nActive clients: {}", limiter.active_clients());
}

Real-World Example: In-Memory Cache

use dashmap::DashMap;
use std::sync::Arc;
use std::time::{Duration, Instant};
use std::hash::Hash;
 
#[derive(Clone)]
struct CacheEntry<T> {
    value: T,
    expires_at: Instant,
}
 
impl<T> CacheEntry<T> {
    fn new(value: T, ttl: Duration) -> Self {
        Self {
            value,
            expires_at: Instant::now() + ttl,
        }
    }
    
    fn is_expired(&self) -> bool {
        Instant::now() > self.expires_at
    }
}
 
struct InMemoryCache<K, V> {
    store: DashMap<K, CacheEntry<V>>,
    default_ttl: Duration,
}
 
impl<K, V> InMemoryCache<K, V>
where
    K: Eq + Hash + Clone,
    V: Clone,
{
    fn new(default_ttl: Duration) -> Self {
        Self {
            store: DashMap::new(),
            default_ttl,
        }
    }
    
    fn with_capacity(default_ttl: Duration, capacity: usize) -> Self {
        Self {
            store: DashMap::with_capacity(capacity),
            default_ttl,
        }
    }
    
    fn get(&self, key: &K) -> Option<V> {
        self.store.get(key).and_then(|entry| {
            if entry.is_expired() {
                None
            } else {
                Some(entry.value.clone())
            }
        })
    }
    
    fn set(&self, key: K, value: V) {
        self.set_with_ttl(key, value, self.default_ttl);
    }
    
    fn set_with_ttl(&self, key: K, value: V, ttl: Duration) {
        let entry = CacheEntry::new(value, ttl);
        self.store.insert(key, entry);
    }
    
    fn delete(&self, key: &K) -> Option<V> {
        self.store.remove(key).map(|(_, entry)| entry.value)
    }
    
    fn contains(&self, key: &K) -> bool {
        self.store.get(key).map(|e| !e.is_expired()).unwrap_or(false)
    }
    
    fn cleanup_expired(&self) -> usize {
        let expired: Vec<_> = self.store.iter()
            .filter(|e| e.value().is_expired())
            .map(|e| e.key().clone())
            .collect();
        
        let count = expired.len();
        for key in expired {
            self.store.remove(&key);
        }
        count
    }
    
    fn len(&self) -> usize {
        self.store.len()
    }
}
 
fn main() {
    let cache: InMemoryCache<String, String> = InMemoryCache::new(Duration::from_secs(60));
    
    // Set values
    cache.set("user:1".to_string(), "Alice".to_string());
    cache.set("user:2".to_string(), "Bob".to_string());
    cache.set_with_ttl("temp".to_string(), "data".to_string(), Duration::from_millis(10));
    
    // Get values
    if let Some(name) = cache.get(&"user:1".to_string()) {
        println!("User 1: {}", name);
    }
    
    println!("Contains user:2: {}", cache.contains(&"user:2".to_string()));
    
    // Wait for temp to expire
    std::thread::sleep(Duration::from_millis(20));
    println!("Contains temp after expiry: {}", cache.contains(&"temp".to_string()));
    
    // Cleanup
    let removed = cache.cleanup_expired();
    println!("Expired entries removed: {}", removed);
    println!("Cache size: {}", cache.len());
}

Real-World Example: Connection Pool

use dashmap::DashMap;
use std::sync::Arc;
use std::collections::VecDeque;
 
struct Connection {
    id: u64,
    endpoint: String,
}
 
impl Connection {
    fn new(id: u64, endpoint: &str) -> Self {
        Self {
            id,
            endpoint: endpoint.to_string(),
        }
    }
    
    fn is_healthy(&self) -> bool {
        // Simulate health check
        true
    }
}
 
struct ConnectionPool {
    pools: DashMap<String, VecDeque<Connection>>,
    max_per_endpoint: usize,
    next_id: std::sync::atomic::AtomicU64,
}
 
impl ConnectionPool {
    fn new(max_per_endpoint: usize) -> Self {
        Self {
            pools: DashMap::new(),
            max_per_endpoint,
            next_id: std::sync::atomic::AtomicU64::new(1),
        }
    }
    
    fn get(&self, endpoint: &str) -> Option<Connection> {
        self.pools.get_mut(endpoint).and_then(|mut pool| {
            pool.pop_front()
        })
    }
    
    fn return_connection(&self, endpoint: &str, conn: Connection) {
        self.pools.entry(endpoint.to_string())
            .or_insert_with(VecDeque::new)
            .push_back(conn);
    }
    
    fn create_if_needed(&self, endpoint: &str) -> Connection {
        let id = self.next_id.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
        Connection::new(id, endpoint)
    }
    
    fn pool_size(&self, endpoint: &str) -> usize {
        self.pools.get(endpoint).map(|p| p.len()).unwrap_or(0)
    }
    
    fn total_connections(&self) -> usize {
        self.pools.iter().map(|e| e.value().len()).sum()
    }
}
 
fn main() {
    let pool = Arc::new(ConnectionPool::new(10));
    
    // Simulate getting connections
    let conn1 = pool.get("api.example.com").unwrap_or_else(|| {
        pool.create_if_needed("api.example.com")
    });
    println!("Got connection {}", conn1.id);
    
    // Return connection
    pool.return_connection("api.example.com", conn1);
    println!("Pool size: {}", pool.pool_size("api.example.com"));
    
    // Get again (should reuse)
    let conn2 = pool.get("api.example.com").unwrap();
    println!("Reused connection {}", conn2.id);
    
    // Different endpoint
    let conn3 = pool.create_if_needed("db.example.com");
    pool.return_connection("db.example.com", conn3);
    
    println!("Total connections: {}", pool.total_connections());
}

Comparison with Mutex

use dashmap::DashMap;
use std::collections::HashMap;
use std::sync::{Arc, Mutex, RwLock};
use std::thread;
use std::time::Instant;
 
fn main() {
    let iterations = 100_000;
    let threads = 8;
    
    // DashMap
    {
        let map: Arc<DashMap<i32, i32>> = Arc::new(DashMap::new());
        for i in 0..threads * 10 {
            map.insert(i, 0);
        }
        
        let start = Instant::now();
        let mut handles = vec![];
        
        for _ in 0..threads {
            let map = Arc::clone(&map);
            handles.push(thread::spawn(move || {
                for i in 0..iterations {
                    map.entry(i % (threads * 10)).and_modify(|v| *v += 1);
                }
            }));
        }
        
        for handle in handles {
            handle.join().unwrap();
        }
        
        println!("DashMap: {:?}", start.elapsed());
    }
    
    // Mutex<HashMap>
    {
        let map: Arc<Mutex<HashMap<i32, i32>>> = Arc::new(Mutex::new(HashMap::new()));
        for i in 0..threads * 10 {
            map.lock().unwrap().insert(i, 0);
        }
        
        let start = Instant::now();
        let mut handles = vec![];
        
        for _ in 0..threads {
            let map = Arc::clone(&map);
            handles.push(thread::spawn(move || {
                for i in 0..iterations {
                    let mut map = map.lock().unwrap();
                    *map.entry(i % (threads * 10)).or_insert(0) += 1;
                }
            }));
        }
        
        for handle in handles {
            handle.join().unwrap();
        }
        
        println!("Mutex<HashMap>: {:?}", start.elapsed());
    }
    
    // RwLock<HashMap>
    {
        let map: Arc<RwLock<HashMap<i32, i32>>> = Arc::new(RwLock::new(HashMap::new()));
        for i in 0..threads * 10 {
            map.write().unwrap().insert(i, 0);
        }
        
        let start = Instant::now();
        let mut handles = vec![];
        
        for _ in 0..threads {
            let map = Arc::clone(&map);
            handles.push(thread::spawn(move || {
                for i in 0..iterations {
                    let mut map = map.write().unwrap();
                    *map.entry(i % (threads * 10)).or_insert(0) += 1;
                }
            }));
        }
        
        for handle in handles {
            handle.join().unwrap();
        }
        
        println!("RwLock<HashMap>: {:?}", start.elapsed());
    }
    
    // DashMap typically outperforms Mutex<HashMap> and RwLock<HashMap>
    // in high-contention scenarios due to sharded locking
}

Real-World Example: Request Deduplication

use dashmap::DashMap;
use std::sync::Arc;
use std::hash::Hash;
 
struct RequestDeduplicator<T> {
    pending: DashMap<T, Arc<std::sync::Condvar>>,
}
 
impl<T> RequestDeduplicator<T>
where
    T: Eq + Hash + Clone,
{
    fn new() -> Self {
        Self {
            pending: DashMap::new(),
        }
    }
    
    fn deduplicate<F, R>(&self, key: T, compute: F) -> R
    where
        F: FnOnce() -> R,
        R: Clone,
    {
        // Check if already pending
        if let Some(_) = self.pending.get(&key) {
            // In real code, you'd wait for the result
            // This is simplified
            println!("Request already in progress for key: {:?}", key);
            return compute();
        }
        
        // Mark as pending
        let condvar = Arc::new(std::sync::Condvar::new());
        self.pending.insert(key.clone(), Arc::clone(&condvar));
        
        // Compute result
        let result = compute();
        
        // Remove from pending
        self.pending.remove(&key);
        condvar.notify_all();
        
        result
    }
    
    fn pending_count(&self) -> usize {
        self.pending.len()
    }
}
 
fn main() {
    let dedup: RequestDeduplicator<String> = RequestDeduplicator::new();
    
    let result1 = dedup.deduplicate("user:1".to_string(), || {
        println!("Computing for user:1");
        "Alice"
    });
    
    println!("Result 1: {}", result1);
    println!("Pending: {}", dedup.pending_count());
}

Summary

  • DashMap provides a thread-safe HashMap with sharded locking for high concurrency
  • Create with DashMap::new(), DashMap::with_capacity(), or DashMap::with_shard_amount()
  • Use insert(), get(), remove() for basic operations (same as HashMap)
  • Use get_mut() for mutable access to values (returns RAII guard)
  • Use entry().or_insert() and entry().and_modify() for atomic updates
  • Iterate with iter() (read-only) or iter_mut() (mutable)
  • Each shard has its own lock, allowing concurrent access to different keys
  • More shards = less contention but more memory overhead
  • Combine with Arc for sharing across threads
  • Use shards() to inspect internal shards for debugging
  • Much faster than Mutex<HashMap> or RwLock<HashMap> in high-contention scenarios
  • Ideal for: caches, rate limiters, connection pools, shared state, request deduplication