How do I use concurrent maps with DashMap in Rust?
Walkthrough
The dashmap crate provides a blazing-fast concurrent hash map for Rust. Unlike standard HashMap wrapped in a Mutex or RwLock, DashMap uses lock sharding to allow multiple threads to access different parts of the map simultaneously with minimal contention. Each shard has its own lock, so operations on different keys typically don't block each other. This makes DashMap ideal for high-concurrency scenarios like web servers, caches, connection pools, and any situation where multiple threads need to read and write to a shared map frequently.
Key concepts:
- Lock sharding — multiple locks for different map regions, reducing contention
- Interior mutability — no need for
Mutex<Map>wrapper - Zero-cost lookups — references returned directly without copying
- Thread-safe by design — Send + Sync out of the box
- Rich API — entry API, iteration, atomic operations
Code Example
# Cargo.toml
[dependencies]
dashmap = "5"use dashmap::DashMap;
fn main() {
let map = DashMap::new();
map.insert("key", "value");
if let Some(value) = map.get("key") {
println!("Found: {}", value);
}
}Creating a DashMap
use dashmap::DashMap;
use std::collections::hash_map::RandomState;
fn main() {
// Create with default settings
let map: DashMap<i32, String> = DashMap::new();
println!("Empty map, len: {}", map.len());
// Create with capacity
let map: DashMap<i32, i32> = DashMap::with_capacity(100);
println!("With capacity 100, len: {}", map.len());
// Create with number of shards (must be power of 2)
// More shards = less contention but more memory overhead
let map: DashMap<i32, i32> = DashMap::with_shards(16);
println!("With 16 shards");
// Create with capacity and shards
let map: DashMap<i32, i32> = DashMap::with_capacity_and_shards(1000, 32);
// With custom hasher
let map: DashMap<i32, i32, RandomState> = DashMap::with_hasher(RandomState::new());
}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!("Length: {}", map.len());
// Get value (returns reference wrapper)
if let Some(value) = map.get("one") {
println!("Got 'one': {}", *value);
}
// Check if key exists
println!("Contains 'two': {}", map.contains_key("two"));
// Remove entry
let removed = map.remove("two");
println!("Removed: {:?}", removed);
// Get and remove atomically
let entry = map.entry("three");
println!("Entry for 'three': {:?}", entry.key());
// Clear all
map.clear();
println!("After clear, len: {}", map.len());
}Working with References
use dashmap::DashMap;
fn main() {
let map: DashMap<i32, String> = DashMap::new();
map.insert(1, "hello".to_string());
map.insert(2, "world".to_string());
// get() returns Option<Ref<K, V>> - a guard that holds the lock
if let Some(ref_value) = map.get(&1) {
// ref_value is like a smart pointer
println!("Key: {}, Value: {}", ref_value.key(), ref_value.value());
// Dereference to access the value
let value: &String = &*ref_value;
println!("Deref'd value: {}", value);
}
// Lock released when ref_value goes out of scope
// get_mut() returns Option<RefMut<K, V>> for mutation
if let Some(mut ref_mut) = map.get_mut(&2) {
ref_mut.value_mut().push_str("!");
println!("Modified: {}", ref_mut.value());
}
}The Entry API
use dashmap::DashMap;
use dashmap::mapref::one::Ref;
fn main() {
let map: DashMap<&str, i32> = DashMap::new();
// entry() returns an Entry enum similar to HashMap
use dashmap::Entry::*;
match map.entry("count") {
Occupied(entry) => {
println!("Already exists: {}", entry.get());
}
Vacant(entry) => {
entry.insert(1);
println!("Inserted new value");
}
}
// or_insert() - insert if vacant
map.entry("counter").or_insert(0);
println!("Counter: {:?}", map.get("counter"));
// or_insert_with() - lazy initialization
map.entry("lazy").or_insert_with(|| {
println!("Computing expensive value...");
42
});
// and_modify() - modify if occupied
map.entry("counter").and_modify(|v| *v += 1);
println!("Counter after modify: {:?}", map.get("counter"));
// or_default() - insert default if vacant
map.entry("default").or_default();
println!("Default: {:?}", map.get("default"));
}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");
// Iterate (locks individual entries as visited)
println!("Iterating:");
for entry in map.iter() {
println!(" {} -> {}", entry.key(), entry.value());
}
// Mutable iteration
println!("\nUppercase:");
for mut entry in map.iter_mut() {
entry.value_mut().make_ascii_uppercase();
println!(" {} -> {}", entry.key(), entry.value());
}
// Keys iterator
println!("\nKeys: {:?}", map.keys().collect::<Vec<_>>());
// Values iterator
println!("Values: {:?}", map.values().collect::<Vec<_>>());
}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 multiple writers
for i in 0..4 {
let map = Arc::clone(&map);
let handle = thread::spawn(move || {
for j in 0..1000 {
let key = format!("thread_{}_key_{}", i, j);
map.insert(key, i * 1000 + j);
}
});
handles.push(handle);
}
// Spawn readers
for _ in 0..2 {
let map = Arc::clone(&map);
let handle = thread::spawn(move || {
for i in 0..500 {
let key = format!("thread_0_key_{}", i);
if let Some(value) = map.get(&key) {
// Do something with value
let _ = *value;
}
}
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
println!("Final map size: {}", map.len());
}Atomic Updates
use dashmap::DashMap;
fn main() {
let map: DashMap<&str, i32> = DashMap::new();
map.insert("counter", 0);
// Update using entry API
for _ in 0..10 {
map.entry("counter").and_modify(|v| *v += 1).or_insert(1);
}
println!("Counter: {:?}", map.get("counter"));
// Fetch and update atomically
let old_value = map.alter("counter", |_, v| v * 2);
println!("After alter: {:?}", map.get("counter"));
// Compare and swap pattern
loop {
if let Some(mut entry) = map.get_mut("counter") {
let current = *entry.value();
if current == 20 {
*entry.value_mut() = 100;
break;
}
}
}
println!("After CAS: {:?}", map.get("counter"));
}Bulk Operations
use dashmap::DashMap;
fn main() {
let map: DashMap<i32, String> = DashMap::new();
// Extend with iterator
map.extend([
(1, "one".to_string()),
(2, "two".to_string()),
(3, "three".to_string()),
]);
println!("After extend: {} entries", map.len());
// Retain only matching entries
map.retain(|k, _| *k < 3);
println!("After retain: {} entries", map.len());
// Shrink to fit (may reduce memory)
map.shrink_to_fit();
// Remove entries matching predicate
map.retain(|_, _| true); // Keep all
}Computing Missing Values
use dashmap::DashMap;
fn main() {
let cache: DashMap<String, u64> = DashMap::new();
fn expensive_computation(key: &str) -> u64 {
println!("Computing for '{}'...", key);
key.len() as u64 * 100
}
// Get or compute
fn get_or_compute(cache: &DashMap<String, u64>, key: &str) -> u64 {
if let Some(value) = cache.get(key) {
return *value;
}
let computed = expensive_computation(key);
cache.insert(key.to_string(), computed);
computed
}
// First call - computes
let val1 = get_or_compute(&cache, "hello");
println!("Result: {}", val1);
// Second call - cached
let val2 = get_or_compute(&cache, "hello");
println!("Result: {}", val2);
// Using entry API for get-or-insert
let val = cache.entry("world".to_string()).or_insert_with(|| {
expensive_computation("world")
});
println!("World: {}", val);
}Web Server Counter Example
use dashmap::DashMap;
use std::sync::Arc;
struct AppState {
counters: DashMap<String, u64>,
}
impl AppState {
fn new() -> Self {
Self {
counters: DashMap::new(),
}
}
fn increment(&self, key: &str) -> u64 {
self.counters
.entry(key.to_string())
.and_modify(|v| *v += 1)
.or_insert(1)
}
fn get(&self, key: &str) -> u64 {
self.counters.get(key).map(|v| *v).unwrap_or(0)
}
fn get_all(&self) -> Vec<(String, u64)> {
self.counters
.iter()
.map(|entry| (entry.key().clone(), *entry.value()))
.collect()
}
}
fn main() {
let state = Arc::new(AppState::new());
// Simulate concurrent requests
let handles: Vec<_> = (0..10)
.map(|i| {
let state = Arc::clone(&state);
std::thread::spawn(move || {
for _ in 0..100 {
state.increment(&format!("user_{}", i % 3));
}
})
})
.collect();
for handle in handles {
handle.join().unwrap();
}
println!("Final counters:");
for (key, count) in state.get_all() {
println!(" {}: {}", key, count);
}
}Connection Pool Example
use dashmap::DashMap;
use std::sync::Arc;
use std::time::{Duration, Instant};
#[derive(Debug)]
struct Connection {
id: u64,
created: Instant,
}
impl Connection {
fn new(id: u64) -> Self {
Self {
id,
created: Instant::now(),
}
}
fn is_stale(&self, max_age: Duration) -> bool {
self.created.elapsed() > max_age
}
}
struct ConnectionPool {
connections: DashMap<String, Connection>,
next_id: std::sync::atomic::AtomicU64,
}
impl ConnectionPool {
fn new() -> Self {
Self {
connections: DashMap::new(),
next_id: std::sync::atomic::AtomicU64::new(1),
}
}
fn get(&self, name: &str) -> Option<Connection> {
self.connections.get(name).map(|c| Connection {
id: c.id,
created: c.created,
})
}
fn get_or_create(&self, name: &str) -> Connection {
let id = self.next_id.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
self.connections
.entry(name.to_string())
.or_insert_with(|| Connection::new(id))
.clone()
}
fn remove_stale(&self, max_age: Duration) {
self.connections.retain(|_, conn| !conn.is_stale(max_age));
}
fn list(&self) -> Vec<(String, u64)> {
self.connections
.iter()
.map(|e| (e.key().clone(), e.id))
.collect()
}
}
fn main() {
let pool = Arc::new(ConnectionPool::new());
// Create connections
pool.get_or_create("db1");
pool.get_or_create("db2");
println!("Connections: {:?}", pool.list());
// Simulate some time passing (connections would be stale)
// In real code, use Instant::now() checks
}Rate Limiter with DashMap
use dashmap::DashMap;
use std::time::{Duration, Instant};
#[derive(Clone)]
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();
self.entries
.entry(client_id.to_string())
.and_modify(|entry| {
// Reset if window expired
if now.duration_since(entry.window_start) > self.window {
entry.count = 0;
entry.window_start = now;
}
entry.count += 1;
})
.or_insert_with(|| RateLimitEntry {
count: 1,
window_start: now,
});
// Check if under limit
self.entries
.get(client_id)
.map(|e| e.count <= self.max_requests)
.unwrap_or(true)
}
pub fn remaining(&self, client_id: &str) -> u32 {
let now = Instant::now();
self.entries
.get(client_id)
.map(|entry| {
if now.duration_since(entry.window_start) > self.window {
self.max_requests
} else {
self.max_requests.saturating_sub(entry.count)
}
})
.unwrap_or(self.max_requests)
}
pub fn cleanup_expired(&self) {
let now = Instant::now();
self.entries.retain(|_, entry| {
now.duration_since(entry.window_start) <= self.window
});
}
}
fn main() {
let limiter = RateLimiter::new(5, Duration::from_secs(60));
let client = "user_123";
for i in 0..7 {
let allowed = limiter.check(client);
let remaining = limiter.remaining(client);
println!("Request {}: allowed={}, remaining={}", i + 1, allowed, remaining);
}
}Session Store Example
use dashmap::DashMap;
use serde::{Deserialize, Serialize};
use std::time::{Duration, Instant};
#[derive(Debug, Clone, Serialize, Deserialize)]
struct Session {
user_id: u64,
username: String,
created_at: Instant,
last_activity: Instant,
data: serde_json::Value,
}
pub struct SessionStore {
sessions: DashMap<String, Session>,
timeout: Duration,
}
impl SessionStore {
pub fn new(timeout: Duration) -> Self {
Self {
sessions: DashMap::new(),
timeout,
}
}
pub fn create_session(&self, user_id: u64, username: String) -> String {
let session_id = uuid::Uuid::new_v4().to_string();
let now = Instant::now();
let session = Session {
user_id,
username,
created_at: now,
last_activity: now,
data: serde_json::json!({}),
};
self.sessions.insert(session_id.clone(), session);
session_id
}
pub fn get_session(&self, session_id: &str) -> Option<Session> {
self.sessions.get(session_id).map(|s| s.clone())
}
pub fn touch_session(&self, session_id: &str) -> bool {
if let Some(mut session) = self.sessions.get_mut(session_id) {
session.last_activity = Instant::now();
true
} else {
false
}
}
pub fn set_session_data(&self, session_id: &str, key: &str, value: serde_json::Value) -> bool {
if let Some(mut session) = self.sessions.get_mut(session_id) {
if let Some(obj) = session.data.as_object_mut() {
obj.insert(key.to_string(), value);
return true;
}
}
false
}
pub fn destroy_session(&self, session_id: &str) {
self.sessions.remove(session_id);
}
pub fn cleanup_expired(&self) {
let now = Instant::now();
self.sessions.retain(|_, session| {
now.duration_since(session.last_activity) < self.timeout
});
}
pub fn session_count(&self) -> usize {
self.sessions.len()
}
}
// Note: This example requires uuid and serde_json dependencies
fn main() {
let store = SessionStore::new(Duration::from_secs(3600));
// Create a session
let session_id = format!("test-session-123");
let now = Instant::now();
store.sessions.insert(session_id.clone(), Session {
user_id: 42,
username: "alice".to_string(),
created_at: now,
last_activity: now,
data: serde_json::json!({"theme": "dark"}),
});
if let Some(session) = store.get_session(&session_id) {
println!("User: {}", session.username);
}
println!("Active sessions: {}", store.session_count());
}Configuration Cache Example
use dashmap::DashMap;
use std::path::PathBuf;
#[derive(Debug, Clone)]
struct Config {
values: DashMap<String, String>,
}
impl Config {
fn new() -> Self {
Self {
values: DashMap::new(),
}
}
fn load_from_file(&self, path: &PathBuf) -> std::io::Result<()> {
let contents = std::fs::read_to_string(path)?;
for line in contents.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
if let Some((key, value)) = line.split_once('=') {
self.values.insert(key.trim().to_string(), value.trim().to_string());
}
}
Ok(())
}
fn get(&self, key: &str) -> Option<String> {
self.values.get(key).map(|v| v.clone())
}
fn set(&self, key: String, value: String) {
self.values.insert(key, value);
}
fn remove(&self, key: &str) -> Option<String> {
self.values.remove(key).map(|(_, v)| v)
}
fn keys(&self) -> Vec<String> {
self.values.keys().map(|k| k.clone()).collect()
}
}
fn main() {
let config = Config::new();
// Set some values
config.set("database.host".to_string(), "localhost".to_string());
config.set("database.port".to_string(), "5432".to_string());
config.set("cache.ttl".to_string(), "3600".to_string());
// Get values
if let Some(host) = config.get("database.host") {
println!("Database host: {}", host);
}
println!("All keys: {:?}", config.keys());
}Metrics Collection
use dashmap::DashMap;
use std::sync::atomic::{AtomicU64, Ordering};
#[derive(Debug)]
struct Metric {
name: String,
counter: AtomicU64,
gauge: AtomicU64,
}
impl Metric {
fn new(name: String) -> Self {
Self {
name,
counter: AtomicU64::new(0),
gauge: AtomicU64::new(0),
}
}
fn increment(&self) {
self.counter.fetch_add(1, Ordering::Relaxed);
}
fn set_gauge(&self, value: u64) {
self.gauge.store(value, Ordering::Relaxed);
}
fn get_counter(&self) -> u64 {
self.counter.load(Ordering::Relaxed)
}
fn get_gauge(&self) -> u64 {
self.gauge.load(Ordering::Relaxed)
}
}
pub struct Metrics {
metrics: DashMap<String, Metric>,
}
impl Metrics {
pub fn new() -> Self {
Self {
metrics: DashMap::new(),
}
}
pub fn increment(&self, name: &str) {
self.metrics
.entry(name.to_string())
.or_insert_with(|| Metric::new(name.to_string()))
.increment();
}
pub fn set_gauge(&self, name: &str, value: u64) {
self.metrics
.entry(name.to_string())
.or_insert_with(|| Metric::new(name.to_string()))
.set_gauge(value);
}
pub fn snapshot(&self) -> Vec<(String, u64, u64)> {
self.metrics
.iter()
.map(|entry| {
(
entry.key().clone(),
entry.get_counter(),
entry.get_gauge(),
)
})
.collect()
}
}
fn main() {
let metrics = Metrics::new();
// Record some metrics
for _ in 0..100 {
metrics.increment("requests_total");
}
metrics.set_gauge("active_connections", 42);
// Get snapshot
println!("Metrics:");
for (name, counter, gauge) in metrics.snapshot() {
println!(" {}: counter={}, gauge={}", name, counter, gauge);
}
}Dedupe Channel Example
use dashmap::DashMap;
use std::sync::Arc;
use std::time::{Duration, Instant};
pub struct DedupeChannel<T> {
seen: DashMap<T, Instant>,
ttl: Duration,
}
impl<T: std::hash::Hash + Eq + Clone> DedupeChannel<T> {
pub fn new(ttl: Duration) -> Self {
Self {
seen: DashMap::new(),
ttl,
}
}
// Returns true if this is a new (not seen) item
pub fn check(&self, item: &T) -> bool {
let now = Instant::now();
if let Some(seen_at) = self.seen.get(item) {
if now.duration_since(*seen_at) < self.ttl {
return false; // Already seen
}
}
self.seen.insert(item.clone(), now);
true
}
pub fn cleanup(&self) {
let now = Instant::now();
self.seen.retain(|_, seen_at| now.duration_since(*seen_at) < self.ttl);
}
pub fn size(&self) -> usize {
self.seen.len()
}
}
fn main() {
let dedupe: Arc<DedupeChannel<String>> = Arc::new(DedupeChannel::new(Duration::from_secs(60)));
// Simulate processing messages
let messages = vec!["msg1", "msg2", "msg1", "msg3", "msg2", "msg4"];
for msg in messages {
if dedupe.check(&msg.to_string()) {
println!("Processing: {}", msg);
} else {
println!("Skipping duplicate: {}", msg);
}
}
}Shard Inspection
use dashmap::DashMap;
fn main() {
let map: DashMap<i32, String> = DashMap::with_shards(4);
// Add some entries
for i in 0..100 {
map.insert(i, format!("value_{}", i));
}
// Get number of shards
println!("Number of shards: {}", map.shards().len());
// Inspect each shard
let mut shard_sizes = vec![];
for shard in map.shards() {
let size = shard.read().len();
shard_sizes.push(size);
}
println!("Shard sizes: {:?}", shard_sizes);
println!("Total entries: {}", shard_sizes.iter().sum::<usize>());
}Comparing with RwLock
use dashmap::DashMap;
use std::collections::HashMap;
use std::sync::{Arc, RwLock};
use std::time::Instant;
fn main() {
let iterations = 100_000;
// DashMap benchmark
let dashmap = Arc::new(DashMap::new());
let start = Instant::now();
let handles: Vec<_> = (0..4)
.map(|t| {
let map = Arc::clone(&dashmap);
std::thread::spawn(move || {
for i in 0..iterations / 4 {
let key = t * (iterations / 4) + i;
map.insert(key, i);
let _ = map.get(&key);
}
})
})
.collect();
for h in handles {
h.join().unwrap();
}
let dashmap_time = start.elapsed();
// RwLock<HashMap> benchmark
let rwlock_map = Arc::new(RwLock::new(HashMap::new()));
let start = Instant::now();
let handles: Vec<_> = (0..4)
.map(|t| {
let map = Arc::clone(&rwlock_map);
std::thread::spawn(move || {
for i in 0..iterations / 4 {
let key = t * (iterations / 4) + i;
{
let mut map = map.write().unwrap();
map.insert(key, i);
}
{
let map = map.read().unwrap();
let _ = map.get(&key);
}
}
})
})
.collect();
for h in handles {
h.join().unwrap();
}
let rwlock_time = start.elapsed();
println!("DashMap: {:?}", dashmap_time);
println!("RwLock<HashMap>: {:?}", rwlock_time);
}Summary
DashMap::new()creates a concurrent hash map with default shardingDashMap::with_shards(n)creates with custom shard count (power of 2)map.insert(k, v)andmap.get(&k)for basic operationsmap.get(&k)returnsOption<Ref<K, V>>— a lock guardmap.get_mut(&k)returnsOption<RefMut<K, V>>for mutationmap.entry(k)provides the familiar entry APImap.iter()andmap.iter_mut()for iterationmap.retain(predicate)for filtering entries- Lock sharding enables high concurrency with minimal contention
- Interior mutability — no
Mutex<Map>wrapper needed - Thread-safe by design (Send + Sync)
- Perfect for: caches, counters, session stores, rate limiters, metrics, connection pools
