Table of Contents

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

  1. Confine or lock every Bodu.Collections type. Wrap it in your own lock, or hand it to one thread. A ReaderWriterLockSlim is only valid for the rows marked read-only safe - never for EvictingDictionary, access-ordered SequencedDictionary, DefaultingDictionary, or DisjointSet, whose reads write.
  2. 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.
  3. Prefer the Bodu.Collections.Concurrent type when one exists - it will beat a locked single-threaded collection under contention and its enumerators are snapshots rather than exceptions.
  4. Share immutable values, not builders. WeekPattern, the option classes, the providers, and the readonly struct results are free to share; PooledBufferBuilder<T> and XorShiftRandom are not.
  5. 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