Shared-State Concurrency
Shared-state concurrency is the model in which multiple threads read and mutate a common in-memory data structure coordinated by mutual-exclusion locks (Mutex<T>, RwLock<T>) or hardware atomic primitives (Atomic*).
Data-Orientated Lock Ownership in Rust
In C, C++, Java, and Go, a mutex (pthread_mutex_t, sync.Mutex) is a standalone object separate from the fields it protects; nothing in the compiler stops a developer from reading or writing a shared struct field while forgetting to lock the mutex first.
In Rust, Mutex<T> and RwLock<T> are generic containers that wrap and own the protected data T. The only way to obtain &T or &mut T is by calling .lock(), .read(), or .write(), which returns a scoped guard (MutexGuard<'_, T>) that releases the lock automatically when dropped:
C / Go Uncoupled Lock (Bug-Prone): Rust Data-Carrying Lock (Compiler-Enforced):
+---------------+ +---------------+ +---------------------------------------+
| sync.Mutex | | balance: i64 | | Mutex<Account> |
+---------------+ +---------------+ | +-------------------------------+ |
(Caller can touch `balance` directly | | Account { balance: i64 } | |
without ever locking `sync.Mutex`!) | +-------------------------------+ |
+---------------------------------------+
(Cannot touch `Account` without `.lock()`!)
Comparing Message Passing and Shared-State Concurrency
| Criterion | Message Passing (mpsc Channels) |
Shared-State (Arc<Mutex<T>>, RwLock<T>, Atomic*) |
|---|---|---|
| Mental model | Single owner per message; ownership flows along a pipeline | Multiple threads access a single canonical state table |
| Contention behavior | Producers enqueue work; consumer processes sequentially | Threads contend directly on cache lines or lock queues |
| Memory footprint | Buffers queued messages in channel memory | Single in-place copy of the shared data structure |
| Deadlock risk | Low (possible only with cyclic bounded sync_channel waits) |
Moderate if multiple locks are acquired in inconsistent order |
| Best suited for | Task queues, actor pipelines, event streaming, I/O fan-in | High-speed in-memory caches, shared connection pools, atomic metrics |
Sharded Locking to Reduce Contention
When a shared HashMap<K, V> is accessed by many threads concurrently, wrapping the entire map in a single Mutex<HashMap<K, V>> serializes operations on unrelated keys. Lock sharding partitions keys across a fixed array of independent Mutex<HashMap<K, V>> buckets ([Mutex<HashMap<K, V>>; N]), multiplying concurrent throughput by N:
use std::collections::HashMap;
use std::hash::{DefaultHasher, Hash, Hasher};
use std::sync::Mutex;
use std::thread;
struct ShardedMap<K, V, const SHARDS: usize = 8> {
shards: [Mutex<HashMap<K, V>>; SHARDS],
}
impl<K: Eq + Hash, V, const SHARDS: usize> ShardedMap<K, V, SHARDS> {
pub fn new() -> Self {
Self {
shards: [const { Mutex::new(HashMap::new()) }; SHARDS],
}
}
fn shard_index(&self, key: &K) -> usize {
let mut hasher = DefaultHasher::new();
key.hash(&mut hasher);
(hasher.finish() as usize) % SHARDS
}
pub fn insert(&self, key: K, value: V) {
let idx = self.shard_index(&key);
self.shards[idx].lock().unwrap().insert(key, value);
}
pub fn get_cloned(&self, key: &K) -> Option<V>
where
V: Clone,
{
let idx = self.shard_index(key);
self.shards[idx].lock().unwrap().get(key).cloned()
}
}
fn main() {
let map: ShardedMap<u32, String, 4> = ShardedMap::new();
thread::scope(|s| {
for id in 0..8 {
s.spawn(move || {
map.insert(id, format!("val-{id}"));
});
}
});
println!("key 3: {:?}, key 7: {:?}", map.get_cloned(&3), map.get_cloned(&7));
}
key 3: Some("val-3"), key 7: Some("val-7")