How to use dashmap crate in Rust concurrent HashMap

Use the dashmap crate to create a concurrent, thread-safe HashMap in Rust for safe multi-threaded data access.

The bottleneck of a single lock

You are building a session store for a web server. Requests arrive on different threads. Thread A checks if a user exists. Thread B updates a user's last-seen timestamp. Thread C deletes an expired session. If you wrap a standard HashMap in a Mutex, every request waits for the previous one to finish, even if they are touching completely different keys. Your server becomes a bottleneck. The lock serializes all access, turning parallel work into a queue.

You need a map that lets threads work on different keys at the same time. You want the safety of Rust's type system without the performance tax of a global lock. That is what dashmap provides. It is a concurrent hash map that allows multiple threads to read and write simultaneously, provided they are accessing different keys.

Sharding: splitting the work

DashMap solves the contention problem by splitting the map into smaller pieces called shards. Think of a massive warehouse with fifty aisles. Each aisle has its own manager holding a key. If you need a box from aisle three, you ask the aisle-three manager. Meanwhile, someone else can grab a box from aisle forty without waiting. The only time you wait is if two people want the same aisle at the same time.

This is sharding. DashMap hashes the key to determine which shard holds the data. It then acquires a lock on just that shard. If the shard is free, you get immediate access. If another thread holds the lock, you wait until it releases. The rest of the map remains available to other threads. This reduces lock contention dramatically compared to a single Mutex.

The trade-off is complexity. A standard HashMap is a single contiguous structure. DashMap is a collection of independent maps behind a hashing layer. This adds overhead for hashing and lock management. You pay a small price for every operation to gain the ability to run operations in parallel.

Minimal example

Add dashmap to your dependencies. The crate is stable and widely used.

[dependencies]
dashmap = "6"

Here is the basic usage pattern. The API mirrors std::collections::HashMap, but with a crucial difference in how values are returned.

use dashmap::DashMap;

fn main() {
    // Create a map. Keys are Strings, values are i32.
    // The map is allocated on the heap.
    let map = DashMap::new();

    // Insert a value. This works like a standard HashMap.
    // The key is hashed to find the shard, then inserted.
    map.insert("counter", 0);

    // Get returns a RefGuard, not a direct value.
    // The guard keeps the shard locked while you read.
    if let Some(guard) = map.get("counter") {
        // Deref the guard to access the value.
        // The guard implements Deref<Target = V>.
        println!("Value: {}", *guard);
    }
    // Guard drops here, releasing the lock immediately.
    // The shard is now free for other threads.
}

The return type of get is a RefGuard. This is not a pointer. It is a RAII wrapper that holds a reference to the shard and prevents the lock from being released until the guard is dropped. This ensures the data stays valid while you use it. If you tried to return a raw reference, the shard could be modified or removed by another thread before you finish reading.

Convention aside: When you clone an Arc<DashMap>, use Arc::clone(&map) explicitly. The community convention is to avoid map.clone() because it looks like a deep clone of the map, but it only clones the Arc. The explicit form signals that you are sharing ownership, not copying data.

The RefGuard: your ticket to the shard

The RefGuard is the core mechanism of DashMap. It bridges the gap between concurrent access and Rust's borrow checker. When you call get, you receive a guard that borrows the shard immutably. When you call get_mut, you receive a guard that borrows the shard mutably.

The guard implements Deref and DerefMut. This allows you to use the guard like a reference. You can read the value, modify it, or call methods on it. The guard also implements Clone. Cloning a guard does not copy the value. It creates another guard pointing to the same shard and bumps an internal reference count. The shard remains locked as long as any guard exists.

This behavior enables safe sharing within a thread. You can pass a guard to a function, clone it, and keep a local copy. The lock stays held until all guards are dropped. If you drop the original guard, the lock does not release if a clone exists. This prevents use-after-free errors even when guards are moved around.

Ah-ha reveal: Cloning a guard is cheap. It does not allocate. It just increments an atomic counter. However, it extends the lock duration. If you clone a guard and pass it to a background task, the shard stays locked until that task finishes. This can block other threads unexpectedly.

Drop the guard the moment you are done. Holding it blocks the shard for every other thread that hashes to the same bucket.

Realistic example: concurrent counters

Here is a realistic scenario. You have a set of counters that multiple threads update concurrently. Each thread increments its own counter repeatedly. DashMap allows all threads to run in parallel because they access different keys.

use dashmap::DashMap;
use std::sync::Arc;
use std::thread;

fn main() {
    // Wrap in Arc to share ownership across threads.
    // DashMap is Send + Sync, so it works with Arc.
    let map = Arc::new(DashMap::new());

    let mut handles = vec![];

    // Spawn 10 threads, each updating different keys.
    for i in 0..10 {
        let map_clone = Arc::clone(&map);
        let handle = thread::spawn(move || {
            // Each thread inserts and updates its own key.
            // Insert is atomic per shard.
            map_clone.insert(i, 0);
            
            for _ in 0..1000 {
                // Get a mutable guard for this thread's key.
                // This locks only the shard containing key i.
                if let Some(mut guard) = map_clone.get_mut(i) {
                    *guard += 1;
                }
                // Guard drops at end of block, releasing lock.
            }
        });
        handles.push(handle);
    }

    // Wait for all threads to finish.
    for handle in handles {
        handle.join().unwrap();
    }

    // Verify results.
    // Iteration locks all shards, so do this after updates are done.
    for i in 0..10 {
        let val = map.get(&i).map(|g| *g).unwrap();
        assert_eq!(val, 1000);
    }
}

The code uses get_mut to obtain a mutable guard. This allows in-place modification. The guard is dropped at the end of the if block, releasing the lock before the next iteration. This minimizes the time the shard is locked.

Convention aside: The pattern map.get(key).map(|g| *g) is idiomatic for extracting a value. It handles the Option and dereferences the guard in one step. It is clearer than if let Some(g) = map.get(key) { *g } else { default } when you just need the value.

Atomic updates with entry

A common pattern is checking if a key exists and inserting a default value if it does not. With a standard HashMap, you can do this atomically with the entry API. DashMap provides the same API, but with shard-aware locking.

use dashmap::DashMap;

fn main() {
    let map = DashMap::new();

    // Use entry to atomically check and insert.
    // This avoids the race condition of get-then-insert.
    let count = map.entry("visits").or_insert(0);
    
    // Modify the value through the guard.
    *count += 1;
    
    // The guard holds the lock until it drops.
    // Other threads waiting for "visits" will block here.
}

The entry API is essential for race-free updates. If you use get followed by insert, another thread might insert the key between the two calls. The entry method locks the shard and performs the check and insertion in one atomic step. This is safer and often faster than separate calls.

Pitfalls: guards, iteration, and resizing

DashMap is powerful, but it has traps. Understanding these prevents subtle bugs and performance issues.

Long-lived guards: The most common mistake is holding a guard across a yield point or a long computation. If you hold a guard while sleeping or waiting for I/O, you block the shard for the entire duration. Other threads cannot access any key in that shard. Always scope guards tightly. Extract the data you need and drop the guard before doing work.

Iteration locks everything: Iterating over a DashMap requires locking all shards. The iterator acquires locks on every shard to ensure a consistent view. This is effectively a global lock. If you iterate while other threads are updating, you serialize all access. Use iteration sparingly. Prefer targeted lookups or snapshots. If you need to process all items, consider collecting keys first, then processing them individually.

Resizing spikes: DashMap resizes when the load factor exceeds a threshold. Resizing creates a new map with more shards and migrates data. This process locks shards and can cause latency spikes under heavy load. If you know the approximate size of your map, pre-size it using DashMap::with_capacity. This avoids resizing during critical phases.

Error inline: If you try to share a DashMap containing Rc values across threads, the compiler rejects you with E0277 (trait bound not satisfied) because Rc is not Send. DashMap requires values to be Send so they can be moved across threads safely. Use Arc instead of Rc for concurrent code.

Deadlock risk: If you hold a guard and call a method that locks the same shard, you deadlock. For example, holding a guard from get and calling insert for a key that hashes to the same shard will block forever. The compiler cannot catch this. You must ensure your code does not nest locks on the same shard.

Pre-size your map. Resizing under load is a performance trap that kills latency guarantees.

Decision matrix

Choosing the right map depends on your concurrency pattern and performance needs. Use the parallel structure below to decide.

Use DashMap when you have high contention on a map and need concurrent access to different keys without a global lock. Use DashMap when multiple threads frequently read and write distinct keys, and you want to maximize throughput. Use DashMap when you need atomic check-and-update operations across threads via the entry API.

Use Mutex<HashMap> when your map is small, contention is low, or you need to iterate over the entire map frequently. Iterating a Mutex<HashMap> only requires one lock acquisition. Iterating a DashMap locks all shards, which is expensive. Use Mutex<HashMap> when you need to perform bulk operations that touch many keys at once.

Use HashMap when you are in a single-threaded context or the data is local to one thread. DashMap adds overhead for sharding and locking that you do not need. A plain HashMap is faster and uses less memory. Use HashMap when performance is critical and concurrency is not required.

Use Arc<Mutex<HashMap>> when you need to share state but updates are rare and reads are dominant, and you want to minimize memory overhead. The Mutex serializes access, but if writes are infrequent, the cost is negligible. This approach uses less memory than DashMap because it does not maintain multiple shard structures.

Sharding helps concurrency, not latency. If you need single-key speed, a HashMap is faster. If you need parallel throughput, DashMap wins. Pick the tool that matches your access pattern.

Where to go next