Thread-safety contracts across Core Foundations
This page is the one-table answer to "can I share this object between threads?" for every public type in Bodu.Core, Bodu.Collections, and Bodu.Collections.Concurrent. Each row is derived from the type's XML documentation and source - the presence or absence of internal synchronization, whether reads mutate state, and how enumeration behaves under concurrent modification - not from a general assumption.
The columns use these terms:
| Term | Meaning |
|---|---|
| Safe | Designed for concurrent use from any number of threads without external locking. |
| Immutable | Cannot change after construction; sharing is trivially safe. |
| Read-only safe | Concurrent reads are safe once no thread is writing; any write (including a read that mutates - see the caveat column) needs exclusive access. |
| Unsafe | No synchronization at all; every access needs an external lock, or the object must stay confined to one thread. |
| Fail-fast | Enumerators capture a structural-version counter and throw InvalidOperationException on the next MoveNext after a structural mutation. Fail-fast detection is a debugging aid on one thread, not a concurrency mechanism. |
| Snapshot | Enumeration runs over a point-in-time copy and never throws for concurrent modification. |
| Undefined | No version counter; mutating during enumeration produces unspecified results. |
Important
"Read-only safe" only holds when no thread mutates. For the collections whose lookups reposition entries (EvictingDictionary under a recency policy, SequencedDictionary in access order, DefaultingDictionary's indexer, DisjointSet.Find), even a lookup is a write - those rows are marked Unsafe rather than read-only safe.
Bodu.Core
| Type | Contract | Enumeration | Notes |
|---|---|---|---|
| WeekPattern | Immutable | Snapshot (value type) | Every operator returns a new value. Guide |
| WorkingDaysOfWeek, CalendarQuarterDefinition, WeekOrdinal, DateTimeResolution, FiscalWeekPattern, IdentifierCase, TitleCaseOptions, SentenceCaseOptions, RandomizationMode, RecursiveSelectControl, AsyncDebouncerExecutionPolicy | Immutable | - | Enums. |
| Option<T>, Result, Result<T>, ResultError, Either<TLeft, TRight> | Immutable | - | readonly struct railway values. Guide |
| Memoizer | Safe | - | The cache is thread-safe; under concurrent first calls for the same argument the wrapped function may run more than once, but only one result is published. Guide |
| AsyncLock, AsyncSemaphore, AsyncReaderWriterLock, AsyncAutoResetEvent, AsyncManualResetEvent, AsyncCountdownEvent, AsyncLazy<T>, AsyncDebouncer, RateGate | Safe | - | Coordination primitives - concurrent use is their purpose. AsyncLazy<T> runs its factory exactly once under contention; RateGate is leading-edge only. Guide |
| PooledBufferBuilder<T> | Unsafe | - | Single-owner builder over a rented array; no synchronization, and Dispose returns the buffer. Guide |
| NaturalStringComparer | Safe | - | Stateless; the static Ordinal / OrdinalIgnoreCase instances may be shared freely. Guide |
| WordCasingOptions, SlugOptions | Immutable | - | init-only properties; build once, share. Guide |
| FiscalWeekQuarterProvider | Immutable | - | All configuration is captured in readonly fields at construction. Guide |
| IQuarterDefinitionProvider, IWeekendDefinitionProvider | Implementation-defined | - | The date extensions call them from whatever thread calls the extension; keep implementations stateless. |
| IRandomGenerator | Implementation-defined | - | Implementations are not required to be thread-safe; helpers that draw from one generator on several threads must pick a safe implementation or lock. |
| XorShiftRandom | Unsafe | - | Non-atomic state updates; concurrent draws corrupt the generator. Use one per thread. |
| SystemRandomAdapter | As the wrapped Random |
- | Safe over Random.Shared; unsafe over a new Random() shared across threads. |
| XmlNamespaceResolver | Immutable | - | Holds only the root's default namespace. |
| StringExtensions, DateTimeExtensions, DateOnlyExtensions, DateTimeFormatInfoExtensions, NumericExtensions, ComparableExtensions, ComparableHelper, EnumExtensions, Enums, ArrayExtensions, SpanExtensions, StreamExtensions, BufferConverter, WorkingDaysOfWeekExtensions, IWeekendDefinitionProviderExtensions, EncodingDetection, EncodingExtensions, StringEncodingExtensions, ThrowHelper, Option, OptionAsyncExtensions, ResultAsyncExtensions, EitherAsyncExtensions | Safe (stateless) | - | Pure static helpers. Enums fills a per-TEnum static cache in a type initializer and hands out copies. They are only as safe as the arguments you pass - an array or stream shared between threads is your problem, not theirs. |
| IEnumerableExtensions | Safe (stateless); results Unsafe | Per operator | The returned sequences are single-consumer iterators - except Cache(), whose buffer is explicitly safe for concurrent enumeration from multiple threads. BatchPooled windows alias one rented buffer and are valid only until the enumerator advances. Guide |
| IListExtensions, IDictionaryExtensions | Safe (stateless); no synchronization of the target | - | GetOrAdd / AddOrUpdate are a lookup then a write - not atomic; use ConcurrentDictionary<TKey,TValue> for shared maps. |
| ShuffleHelpers, SequenceGenerator | Safe (stateless); sequences Unsafe | - | Generated sequences carry per-enumerator state only; ShuffleHelpers is as safe as the IRandomGenerator it is given. |
Bodu.Collections
Every mutable collection in this package is single-threaded by design: none takes locks, and the non-generic ICollection.IsSynchronized is always false. Their enumerators are fail-fast unless the table says otherwise.
| Type | Contract | Enumeration | Read-mutates caveat / notes |
|---|---|---|---|
| RingBackedCollection<T> | Unsafe | Fail-fast (struct enumerator) | Abstract base; the version counter is bumped inside every protected mutator. Guide |
| CircularBuffer<T> | Unsafe | Fail-fast | Eviction events run inline; mutating from a handler throws. Use ConcurrentCircularBuffer<T> for MPMC. Guide |
| Deque<T> | Unsafe | Fail-fast | Same eviction-handler rule under EvictOpposite. Guide |
| EvictingDictionary<TKey, TValue> | Unsafe - reads mutate | Fail-fast; expired entries are filtered against a clock snapshot taken at enumerator creation | Every lookup (this[key], TryGetValue, ContainsKey, …) counts as a touch: LRU / LFU-style policies reposition the entry, and with an expiration configured an access may purge expired keys. Even concurrent readers race. Guide |
| EvictingDictionaryExpiration | Immutable | - | Expiration configuration value. |
| SequencedDictionary<TKey, TValue> | Read-only safe in insertion order; Unsafe in access order | Fail-fast | In access-order mode a lookup moves the entry to the end. Guide |
| BiDictionary<TKey, TValue> | Read-only safe | Fail-fast (BCL-dictionary rules, through either view) | The Inverse view shares storage - a write through one view is a write to both. Guide |
| LayeredDictionary<TKey, TValue> | Read-only safe | Keys / Values are snapshots; enumeration follows each layer's own rules |
Writes go to the first layer only; the layers themselves are ordinary dictionaries you also must not mutate concurrently. Guide |
| DefaultingDictionary<TKey, TValue> | Unsafe - indexer reads mutate | Fail-fast (wrapped dictionary); enumeration never materializes defaults | this[key] on a missing key invokes the factory and stores the result; the check-invoke-store sequence is not atomic. Guide |
| Table<TRow, TColumn, TValue> | Read-only safe | Fail-fast (dictionary rules) via the live Row / Column views; ColumnKeys is a snapshot |
Guide |
| IndexedPriorityQueue<TElement, TPriority> | Read-only safe | Fail-fast (also bumped by re-prioritization) | Guide |
| IndexedSet<T>, OrderedSet<T> | Read-only safe | Fail-fast | The set-algebra members (UnionWith, …) mutate in place. Guide |
| NavigableSet<T>, NavigableDictionary<TKey, TValue> | Read-only safe | Fail-fast, including every ascending / descending / range view and the key and value collections | Views are live: they reflect later mutations on their next iteration. Guides · dictionary |
| Multiset<T> | Read-only safe | Fail-fast | Guide |
| MultiValueDictionary<TKey, TValue> | Read-only safe | Fail-fast | Guide |
| Range<T>, ValueRange<TKey, TValue> | Immutable | - | readonly struct keys and entries. Guide |
| RangeDictionary<TKey, TValue>, RangeSet<T> | Read-only safe | Fail-fast | Guide |
| IntervalTree<T>, IntervalTree<TKey, TValue> | Read-only safe | Fail-fast, including QueryPoint / QueryOverlaps results while they are being iterated |
Guide |
| SegmentedBuffer<T> | Read-only safe | Fail-fast | Append-only. Guide |
| BitSet | Read-only safe | Fail-fast | The in-place logical operators (And, Or, …) are writes. Guide |
| BloomFilter<T>, CountMinSketch<T>, HyperLogLog<T> | Read-only safe | - (no element enumeration) | Add / Merge / Import are writes; Export produces a versioned snapshot. Guide |
| Graph<T> | Read-only safe | Undefined - no version counter; do not mutate while iterating Vertices or Neighbors |
Algorithms in GraphAlgorithms only read. Guide |
| IReadOnlyGraph<TVertex>, IReadOnlyWeightedGraph<TVertex> | As the implementation | - | Read-only interfaces, not immutable views - the underlying graph may still be mutated by its owner. |
| GraphAlgorithms | Safe (stateless) | - | Operates on the graph it is given; the graph must not be mutated during a run. |
| ShortestPathResult<TVertex> | Immutable | - | readonly record struct result. |
| DisjointSet<T> | Unsafe - Find mutates |
- (no public enumeration) | Path-halving compression rewrites parent links on every Find / TryFind / AreConnected, so even concurrent lookups race. Guide |
| Trie, Trie<TValue>, RadixTrie, RadixTrie<TValue> | Read-only safe | Fail-fast, including prefix-query sequences | Guide |
| AhoCorasickAutomaton, AhoCorasickAutomaton<TValue> | Immutable | - | Built once from the full pattern set (a new set means a new automaton); matching is a pure read and is safe to run concurrently. Guide |
| AhoCorasickMatch, AhoCorasickMatch<TValue> | Immutable | - | readonly record struct results. |
| Tree<T> | Read-only safe | Undefined - traversals carry no version; snapshot with ToList() before mutating |
Depth / Height are computed reads. Guide |
| DequeOverflowPolicy, EvictingDictionaryPolicy, EvictingDictionaryExpirationKind, BiDictionaryDuplicateValuePolicy, MultiValueBacking, GraphKind | Immutable | - | Enums. |
Bodu.Collections.Concurrent
| Type | Contract | Enumeration | Notes |
|---|---|---|---|
| ConcurrentCircularBuffer<T> | Safe - lock-free MPMC (Vyukov ring) | Snapshot (atomic copy at GetEnumerator); never throws for concurrent modification |
Count may be transiently stale under contention. Like its two siblings it reports ICollection.IsSynchronized == false - there is no lock to expose because the type manages its own synchronization, so do not try to coordinate access through SyncRoot. Guide |
| ConcurrentHashSet<T> | Safe - lock-free split-ordered list | Weakly consistent snapshot: elements added or removed after the snapshot completes are not seen; elements changing during capture may or may not appear | Resizes are lock-free and never invalidate readers. The ISet<T> bulk operations (UnionWith, IntersectWith, …) are sequences of individual atomic steps, not one atomic operation. Guide |
| ConcurrentEvictingDictionary<TKey, TValue> | Safe - lock-striped segments | Point-in-time snapshot; Keys / Values are snapshots too (each read allocates) |
GetOrAdd is single-flight per key (the factory runs under the owning segment's lock, at most once per key even under concurrent misses); ItemEvicted is raised after the lock is released. Reads still touch eviction state, but the striping makes that safe. Guide |
| ConcurrentLruCache<TKey, TValue> | Safe - lock-free reads | Point-in-time snapshot of the backing dictionary; order unspecified | Reads take no lock; queue maintenance is amortized onto writers. GetOrAdd follows ConcurrentDictionary semantics - racing callers may each run the factory, so it is not a stampede guard. Count may transiently exceed Capacity by at most the number of in-flight writers. Guide |
Rules of thumb
- Confine or lock every
Bodu.Collectionstype. Wrap it in your own lock, or hand it to one thread. AReaderWriterLockSlimis only valid for the rows marked read-only safe - never forEvictingDictionary, access-orderedSequencedDictionary,DefaultingDictionary, orDisjointSet, whose reads write. - Do not rely on fail-fast to detect races. The version check runs on the enumerating thread and can miss interleavings; it exists to turn a same-thread bug into an exception.
- Prefer the
Bodu.Collections.Concurrenttype when one exists - it will beat a locked single-threaded collection under contention and its enumerators are snapshots rather than exceptions. - Share immutable values, not builders.
WeekPattern, the option classes, the providers, and thereadonly structresults are free to share;PooledBufferBuilder<T>andXorShiftRandomare not. - Stateless helpers are safe; their arguments may not be. Every extension class is pure, so thread safety collapses to the thread safety of the object you pass in.
Where to go next
- Concurrent collections - the three safe collections in depth.
- Async coordination primitives - the locks and gates for coordinating the rest.
- Choosing a collection - picking by requirement, including "shared between threads".
- Extending
RingBackedCollection<T>- the version counter and fail-fast contract from the implementer's side. - Core Foundations guides - every guide in this topic.