Every dictionary backend in liblevenshtein (via libdictenstein) is Send + Sync
and cheap to share across threads — a clone is an Arc bump, not a deep copy. What
differs between backends is how concurrent reads and writes are coordinated: every
dictionary read is lock-free or wait-free (a reader never blocks), and writers publish new
state by an atomic pointer swap or CAS rather than by excluding readers.
Concurrency model — dictionary reads grouped by lock-free read mechanism.
SyncStrategy contractEach backend reports a SyncStrategy from dictionary.sync_strategy(), telling the
caller what coordination it provides:
SyncStrategy | Meaning |
|---|---|
Persistent | An immutable / structural-sharing snapshot. Reads need no synchronization; writes (if any) publish a new version atomically. |
InternalSync | The backend is internally synchronized for concurrent access — atomic operations or lock-free structures. Reads are lock-free. |
ExternalSync | The backend uses interior mutability guarded by an internal lock (a parking_lot::RwLock). Reads take a shared lock and block only against an active writer. |
The enum is a coarse hint, not a precise read-cost model. For example
DoubleArrayTriereportsExternalSync(the trait default) yet is immutable after build, so its reads never block. The grouping below is by actual read behaviour.
| Backend | Read path | Reads block? | Writes | Kind |
|---|---|---|---|---|
DoubleArrayTrie(Char) | immutable base/check arrays | No | build-time only (&mut self) | static |
DynamicDawgU64 | immutable root revision retained by Arc | No | path-copy + root compare_exchange (&self) | dynamic |
PathMapSnapshot(Char) / PathMapRef(Char) | persistent copy-on-write snapshot | No | — (immutable view) | static snapshot |
PersistentARTrie(Char / U64) | lock-free CAS overlay over a memory-mapped trie | No | lock-free CAS (&self) | dynamic · disk |
PersistentScdawg(Char) | ArcSwap graph load | No | ArcSwap publish (&self) | dynamic · disk |
PersistentSuffixAutomaton(Char) | ArcSwap graph load | No | ArcSwap publish (&self) | dynamic · disk |
PersistentSuffixTree(Char) | ArcSwap graph load | No | ArcSwap publish (&self) | dynamic · disk |
PersistentVocabARTrie | lock-free overlay | No | lock-free overlay (&self) | dynamic · disk |
DynamicDawg(Char) | immutable root revision retained by Arc (LockFreeDawg core) | No | path-copy + root compare_exchange (&self) | dynamic |
SuffixAutomaton(Char) | Arc<ArcSwap<…>> load (LockFreeSuffixAutomaton) | No | ArcSwap publish (&self) | dynamic |
Scdawg(Char) | Arc<ArcSwap<…>> load (LockFreeScdawg) | No | ArcSwap publish (&self) | dynamic |
PathMapDictionary(Char) | Arc<ArcSwap<PathMapState>> load, then lock-free traversal | No | ArcSwap publish (&self) | dynamic |
BijectiveMap | forward DynamicDawgChar (lock-free) + reverse Arc<ArcSwap<HashMap>> | No | ArcSwap publish (&self) | dynamic |
A reader never blocks on any dictionary backend. Every in-memory backend loads its state
through an arc-swap guard (or reads immutable arrays), and writers publish new state by
an atomic pointer swap or CAS rather than by excluding readers. Grouped by mechanism:
DoubleArrayTrie(Char) — immutable after build; reads touch read-only arrays
(wait-free).DynamicDawg(Char), DynamicDawgU64 — the LockFreeDawg core: reads retain
one immutable root revision, while writers path-copy the changed route and
compare_exchange a replacement GraphVersion. (DynamicDawg uses u8 edge
labels; DynamicDawgU64 a wider u64 label.)SuffixAutomaton(Char), Scdawg(Char) — the LockFreeSuffixAutomaton /
LockFreeScdawg cores: reads load an Arc<ArcSwap<…>> graph snapshot; writes publish
a new graph by atomic swap.PathMapDictionary(Char) — reads load an Arc<ArcSwap<PathMapState>> snapshot and
walk it lock-free; writes publish a new state by atomic swap.BijectiveMap — a lock-free forward DynamicDawgChar plus a reverse
Arc<ArcSwap<HashMap>>; both directions publish by atomic swap.PathMapSnapshot(Char), PathMapRef(Char)) — a persistent,
copy-on-write view that holds no lock.Persistent* family — PersistentARTrie(Char/U64),
PersistentScdawg(Char), PersistentSuffixAutomaton(Char),
PersistentSuffixTree(Char), PersistentVocabARTrie — all read through a lock-free
CAS / ArcSwap overlay over memory-mapped storage (durable and lock-free).Historical note. Earlier releases guarded the three in-memory dynamic backends (
DynamicDawg,SuffixAutomaton,Scdawg),PathMapDictionary, andBijectiveMapwith aparking_lot::RwLock, so a writer briefly excluded all readers. They have since adopted the same lock-free reader model asDynamicDawgU64and thePersistent*family: every dictionary backend now reportsSyncStrategy::InternalSync(orPersistent), and no dictionary read blocks on a writer. Theparking_lot/std::syncRwLockdistinction now applies only to internal coordination inside the disk-backedpersistent_artrieengine, never to a dictionary read path.
Every dictionary backend now uses the lock-free ArcSwap read path on the left. The
parking_lot::RwLock path on the right is the model the in-memory backends used in
earlier releases — and the one the disk-backed persistent_artrie engine still uses for
internal coordination. The contrast is what motivated the migration:
Lock-free ArcSwap reads (now used by every dictionary backend) vs. the parking_lot
RwLock reads used by the in-memory backends in earlier releases and still used for
internal coordination inside the disk-backed persistent_artrie engine.
Any backend can be shared and queried concurrently — clone the Transducer (an Arc
bump) and move clones into threads:
use liblevenshtein::prelude::*;
use std::thread;
let dict = DoubleArrayTrie::from_terms(vec!["test", "testing", "tester"]);
let transducer = Transducer::new(dict, Algorithm::Standard);
let handles: Vec<_> = ["tset", "tesing", "testr"]
.into_iter()
.map(|q| {
let t = transducer.clone(); // cheap: Arc clone
thread::spawn(move || t.query(q, 2).collect::<Vec<_>>())
})
.collect();
for h in handles {
for term in h.join().expect("query thread") {
println!("{term}");
}
}
On every backend those queries never block one another: DoubleArrayTrie reads immutable
arrays, and each dynamic backend (DynamicDawg, DynamicDawgU64, SuffixAutomaton,
Scdawg, PathMapDictionary, BijectiveMap) and Persistent* type reads through a
lock-free ArcSwap / CAS snapshot. A concurrent writer publishes new state by an atomic
swap and never excludes readers.
Dynamic backends accept writes through &self (interior mutability), so a shared
handle can be mutated while others query:
let dict = DynamicDawg::from_terms(vec!["alpha", "beta"]);
let writer = dict.clone();
thread::spawn(move || { writer.insert("gamma"); }); // lock-free: publishes via atomic swap
// Other threads keep querying without ever blocking; they observe the update as soon as
// the writer's atomic swap completes. Reads never wait on a writer.
for term in dict.query("gama", 1) { println!("{term}"); }
ArcSwap)PathMapDictionary holds its state in an Arc<ArcSwap<…>>, so reads are lock-free
and a write publishes a new state by an atomic pointer swap — readers never block:
pub struct PathMapDictionary<V: DictionaryValue = ()> {
state: Arc<ArcSwap<PathMapState<V>>>,
}
A reader loads the current state through an arc-swap guard (no lock taken); a writer
builds the next state from a persistent, structurally shared copy of the trie and swaps it
in with a single atomic store. This is the same copy-on-write discipline exposed by the
PathMapSnapshot / PathMapRef views.
Historical note. Earlier releases wrapped the upstream PathMap in
Arc<RwLock<…>>. Under that design the concurrency tests (tests/concurrency_test.rs) measured ~3.82× read throughput on 8 threads — readers did not block one another and queries proceeded during interleaved writes, the expected profile of a reader–writer lock with a short write critical section. Moving toArcSwapremoves the read lock entirely, so readers no longer contend even momentarily.
DynamicDawg, DynamicDawgU64, or PathMapDictionary — or a Persistent*
type (durable). Readers never block on a writer.DoubleArrayTrie (immutable after build).DynamicDawg (byte) or DynamicDawgChar (Unicode) — lock-free
reads with compare_exchange writes; DynamicDawgU64 trades a wider u64 edge label
for the same guarantees.SuffixAutomaton / Scdawg (lock-free) in
memory, or PersistentSuffixAutomaton / PersistentScdawg (lock-free, durable) on disk.Can you improve this documentation?Edit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |