Table of Contents

ConcurrentCircularBuffer<T> Class

Definition

Namespace
Bodu.Collections.Generic.Concurrent
Assembly
Bodu.Collections.Concurrent.dll
Package
Bodu.Collections.Concurrent 1.0.0
Source
ConcurrentCircularBuffer{T}.Enumerator.cs

Provides a lock-free, bounded first-in, first-out (FIFO) buffer with optional overwrite semantics.

public sealed class ConcurrentCircularBuffer<T> : IProducerConsumerCollection<T>, ICollection, IReadOnlyCollection<T>, IEnumerable<T>, IEnumerable where T : class?

Type Parameters

T

The reference type stored in the buffer (constraint: where T : class).

Inheritance
ConcurrentCircularBuffer<T>
Implements
Inherited Members
Extension Methods

Examples

var buffer = new ConcurrentCircularBuffer<string>(capacity: 3, allowOverwrite: true);
buffer.Enqueue("A");
buffer.Enqueue("B");
buffer.Enqueue("C");
buffer.Enqueue("D"); // "A" is evicted

if (buffer.TryPeek(out var head))
    Console.WriteLine(head); // "B"

Console.WriteLine(buffer.Dequeue()); // "B"

Remarks

This type implements a multi-producer/multi-consumer (MPMC) circular buffer using per-slot sequence numbers (Vyukov pattern). The buffer capacity is fixed at construction time and must be at least 2.

The Vyukov MPMC sequence protocol uses two distinct sequence marks per slot: one written by the producer when data is published (tail + 1), and one written by the consumer when the slot is released (head + capacity). These marks must be numerically distinct so that concurrent producers can determine whether a slot is free or still occupied. With a capacity of 1 they are always equal for every round, making the two states indistinguishable and allowing a second concurrent producer to overwrite an occupied slot, permanently skipping a sequence number and leaving consumers in an infinite spin. A minimum capacity of 2 is therefore required for the protocol to be correct.

When AllowOverwrite is true, enqueuing into a full buffer evicts the oldest element and raises the ItemEvicted event (after removal). Ordinary handler exceptions are caught and suppressed (a process-fatal OutOfMemoryException still propagates); this differs intentionally from the non-concurrent CircularBuffer<T>, which propagates handler exceptions. Under MPMC the eviction has already been committed by the time the event fires (the head counter has advanced and the slot has been freed by another consumer or producer running in parallel), so propagating a handler exception cannot abort the eviction and would only obscure the cause of failure for unrelated callers. Subscribers that need to react to a throwing handler should perform their own catch/log in the handler body.

The Count property is approximate under concurrency. For a stable point-in-time view of contents, use ToArray().

Enumeration iterates over a true snapshot captured at the moment the enumerator is created and does not reflect subsequent changes.

Slots are padded to 64 bytes - the standard cache-line size on x86/x64 hardware - to prevent false sharing between adjacent producer- and consumer-touched slots. Targets with larger cache lines (notably Apple Silicon and some ARM SoCs at 128 bytes) remain correct; the padding is conservative on those platforms but does not degrade throughput.

The generic type parameter is constrained to class? because the slot value is published and cleared through Volatile overloads that target reference types. Value-type element support would require a different publication mechanism and is intentionally out of scope for this type.

Any capacity of at least two is supported, including non-power-of-two values, and Capacity reports the exact requested value. One caveat applies at extreme longevity: the head and tail counters are 32-bit, and the physical slot index is their unsigned modulo the capacity. When the capacity is not a power of two, the counter's wrap at 2^32 operations shifts that modulo by 2^32 mod capacity, which could misalign a single slot once every ~4.29 billion enqueue/dequeue operations. A power-of-two capacity divides 2^32 evenly and is therefore free of the caveat entirely; callers whose buffers process on the order of billions of operations without recreation should prefer one. Eliminating the caveat for arbitrary capacities would require widening the counters to 64-bit and is intentionally out of scope for this type.

Constructors

ConcurrentCircularBuffer()

Initializes a new instance of the ConcurrentCircularBuffer<T> class with default capacity and overwriting enabled.

public ConcurrentCircularBuffer()

ConcurrentCircularBuffer(IEnumerable<T>, int, bool)

Initializes a new instance of the ConcurrentCircularBuffer<T> class. Initializes a new instance by copying from collection, using the specified capacity and overwrite behavior.

public ConcurrentCircularBuffer(IEnumerable<T> collection, int capacity, bool allowOverwrite = true)

Parameters

collection IEnumerable<T>

The collection whose elements are copied into the buffer. Must not be null.

capacity int

The maximum number of elements the buffer can hold. Must be at least 2. See the class remarks for an explanation of why the Vyukov MPMC protocol requires a minimum capacity of 2.

allowOverwrite bool

true to evict the oldest element when the buffer is full; false to throw on overflow. Defaults to true.

Exceptions

ArgumentNullException

collection is null.

ArgumentOutOfRangeException

capacity < 2.

InvalidOperationException

allowOverwrite is false and the number of items in collection exceeds capacity.

ConcurrentCircularBuffer(int)

Initializes a new instance of the ConcurrentCircularBuffer<T> class with the specified capacity and overwriting enabled.

public ConcurrentCircularBuffer(int capacity)

Parameters

capacity int

The maximum number of elements the buffer can hold. Must be at least 2. See the class remarks for an explanation of why the Vyukov MPMC protocol requires a minimum capacity of 2.

Exceptions

ArgumentOutOfRangeException

capacity < 2.

ConcurrentCircularBuffer(int, bool)

Initializes a new instance of the ConcurrentCircularBuffer<T> class with the specified capacity and overwrite behavior.

public ConcurrentCircularBuffer(int capacity, bool allowOverwrite)

Parameters

capacity int

The maximum number of elements the buffer can hold. Must be at least 2. See the class remarks for an explanation of why the Vyukov MPMC protocol requires a minimum capacity of 2.

allowOverwrite bool

true to evict the oldest element when the buffer is full; false to throw or return false instead.

Exceptions

ArgumentOutOfRangeException

capacity < 2.

Properties

AllowOverwrite

Gets or sets a value indicating whether enqueuing into a full buffer evicts the oldest element.

public bool AllowOverwrite { get; set; }

Property Value

bool

true to evict the oldest element when full; false to throw or return false from the producer-side methods.

Remarks

Toggling this property concurrently with an in-flight Enqueue(T) or TryEnqueue(T) is safe but may have a benign window where a producer that observed the previous value commits its behavior (eviction or rejection) before the new value takes effect. The window does not corrupt buffer state.

Capacity

Gets the fixed capacity of the buffer.

public int Capacity { get; }

Property Value

int

Count

Gets an approximate count of elements currently contained in the buffer.

public int Count { get; }

Property Value

int

The number of elements observed at the time of the call. Under concurrency, the value may be transiently stale. For a stable point-in-time count, call ToArray() and use its length.

Remarks

The count is computed as the difference between the tail and head positions, clamped to the range [0, Capacity]. Because these two positions are read independently, the result is approximate.

The clamping guards against both the transient appearance of a negative difference under concurrent modification and the correct handling of signed counter wrapping after sustained high-volume use.

this[int]

Gets the element at the specified zero-based logical index relative to the oldest element.

public T this[int index] { get; }

Parameters

index int

The zero-based index of the element to retrieve. Must be non-negative and less than Count observed at the moment of the call.

Property Value

T

The element that was at the specified logical position during the call.

Remarks

The accessor performs a sequence-validated single-slot read; it does not allocate a snapshot. Two consecutive index reads (for example, buffer[i] followed by buffer[i + 1]) are not jointly atomic - concurrent producers or consumers may modify the buffer between the two reads. Callers that require joint atomicity across multiple positions should call ToArray() once and index the resulting array.

Exceptions

ArgumentOutOfRangeException

index is negative, or is greater than or equal to the number of elements in the buffer observed at the time of the call.

InvalidOperationException

The buffer is under sustained concurrent modification and a stable single-slot read could not be obtained within the retry budget.

Methods

Clear()

Removes elements from the buffer up to the number present at the time of the call.

public void Clear()

Remarks

This method drains at most the number of elements observed when Clear() is called. Elements added by concurrent producers after this point are not removed. This bounding prevents an indefinite loop when AllowOverwrite is true and producers are continuously enqueueing.

The ItemEvicted event is not raised by this operation, as clearing the buffer is not considered an eviction.

Contains(T?)

Determines whether the buffer contains the specified element using Default.

public bool Contains(T? item)

Parameters

item T

The element to locate. May be null.

Returns

bool

true if a sequence-stable read found a match within the live region during the call; otherwise false.

Remarks

Walks the live region using a sequence-validated direct slot scan; no array allocation occurs in the common case. Under sustained concurrent modification the scan restarts; if the retry budget is exhausted, a single coherent ToArray() snapshot is used as a fallback.

CopyTo(T[], int)

Copies a snapshot of the buffer to array starting at index.

public void CopyTo(T[] array, int index)

Parameters

array T[]

The destination array. Must not be null.

index int

The zero-based index in array at which copying begins.

Exceptions

ArgumentNullException

array is null.

ArgumentOutOfRangeException

index is less than zero.

ArgumentException

The number of elements in the buffer exceeds the available space in array from index onward.

Dequeue()

Removes and returns the oldest element.

public T Dequeue()

Returns

T

The oldest element in the buffer.

Exceptions

InvalidOperationException

The buffer is empty.

Enqueue(T)

Adds an element to the end of the buffer, throwing when full if overwriting is disabled.

public void Enqueue(T item)

Parameters

item T

The element to add. May be null.

Exceptions

InvalidOperationException

The buffer is full and AllowOverwrite is false.

GetEnumerator()

Returns an enumerator that iterates over a point-in-time snapshot of the buffer's contents.

public ConcurrentCircularBuffer<T>.Enumerator GetEnumerator()

Returns

ConcurrentCircularBuffer<T>.Enumerator

An ConcurrentCircularBuffer<T>.Enumerator that iterates through the elements in FIFO order, from oldest to newest, as they existed at the moment the enumerator was created.

Remarks

Enumeration operates on an atomic snapshot captured via ToArray() at the moment this method is called. Any elements enqueued or dequeued after the enumerator is created are not reflected in the enumerated sequence.

Because the enumerator operates on a snapshot, it will never throw InvalidOperationException due to concurrent modification - unlike enumerators on non-concurrent collections.

Peek()

Returns the oldest element without removing it.

public T Peek()

Returns

T

The oldest element in the buffer.

Exceptions

InvalidOperationException

The buffer is empty.

ToArray()

Returns a snapshot of the buffer's contents in FIFO order.

public T[] ToArray()

Returns

T[]

An array containing the elements observed in the buffer, ordered from oldest to newest. Returns an empty array if the buffer is empty.

Remarks

Each slot in the snapshot is read using a sequence-validated seqlock pattern: the slot's coordination sequence is read both before and after the value, and the read is committed only when both sequence observations match the expected published mark. This guarantees the value, when committed, was the element published at that logical position - never a value from an earlier or later generation.

If a slot cannot be stabilized within its retry budget, the entire snapshot is restarted. After the outer retry budget is exhausted under sustained churn, a best-effort snapshot is returned in which individual slots that still cannot be stabilized contribute the default value of T; every committed slot in that fallback path is still sequence-validated, so a torn or stale-generation reference is never returned.

TryDequeue(out T?)

Attempts to remove and return the oldest element.

public bool TryDequeue(out T? item)

Parameters

item T

When this method returns true, contains the removed element; otherwise, null.

Returns

bool

true if an element was successfully removed; false if the buffer was empty.

TryEnqueue(T)

Attempts to add an element to the end of the buffer without throwing when full.

public bool TryEnqueue(T item)

Parameters

item T

The element to add. May be null.

Returns

bool

true if the element was enqueued; false if the buffer is full and AllowOverwrite is false.

TryPeek(out T?)

Attempts to return the oldest element without removing it.

public bool TryPeek(out T? item)

Parameters

item T

When this method returns true, contains the oldest element; otherwise, null.

Returns

bool

true if an element was found; false if the buffer was empty.

Remarks

This method retries on transient races where another thread concurrently dequeues the head element between the head position read and the slot sequence check. It returns false only when the buffer is observed to be empty.

Events

ItemEvicted

Occurs immediately after an item has been evicted because a new item was enqueued into a full buffer while AllowOverwrite is true.

public event Action<T>? ItemEvicted

Event Type

Action<T>

Remarks

Ordinary exceptions thrown by handlers are caught and suppressed; a process-fatal OutOfMemoryException is allowed to propagate to the caller of Enqueue(T) rather than being masked. Each subscriber's invocation is guarded independently so that a throwing handler cannot prevent later handlers from receiving the notification.

This differs from the non-concurrent ItemEvicted, which propagates handler exceptions to the caller of Enqueue(T). Under MPMC the eviction has already been committed by the time this event fires, so propagating the exception would block the caller for a failure unrelated to their own write and could not undo the eviction.

Count under contention. When multiple producers overwrite a full buffer concurrently, a single logical "enqueue into a full buffer" can trigger more than one eviction: a producer may evict the oldest element to free a slot, lose that freed slot to another producer before it can claim it, and evict again. Each eviction still removes a distinct real element in FIFO order and fires this event exactly once, so data is never lost or duplicated - but the total number of firings is an upper bound on the number of admissions, not a one-to-one signal. A subscriber counting evictions against enqueues will see them diverge under write pressure. In the uncontended (single-producer) case the ratio is exactly one eviction per overflow admission. Handlers run inside the lock-free dequeue path, so they must not perform heavy work.

Explicit Interface Implementations

IProducerConsumerCollection<T>.TryAdd(T)

Attempts to add item to the buffer using the producer-side path.

bool IProducerConsumerCollection<T>.TryAdd(T item)

Parameters

item T

The element to add. May be null.

Returns

bool

true if the element was enqueued; false if the buffer is full and AllowOverwrite is false.

Remarks

Forwards to TryEnqueue(T). Provided to satisfy IProducerConsumerCollection<T> so the buffer can be wrapped by BlockingCollection<T> for blocking-consumer scenarios.

IProducerConsumerCollection<T>.TryTake(out T)

Attempts to remove and return the oldest element from the buffer.

bool IProducerConsumerCollection<T>.TryTake(out T item)

Parameters

item T

When this method returns true, contains the removed element; when it returns false, contains the default value of T.

Returns

bool

true if an element was removed; otherwise false.

Remarks

Forwards to TryDequeue(out T?). Because T is constrained to a reference type, the MaybeNullWhenAttribute on the interface signature is honored: the out parameter is null when the method returns false.

IEnumerable<T>.GetEnumerator()

Returns an enumerator that iterates over a point-in-time snapshot of the buffer's contents.

IEnumerator<T> IEnumerable<T>.GetEnumerator()

Returns

IEnumerator<T>

An ConcurrentCircularBuffer<T>.Enumerator that iterates through the elements in FIFO order, from oldest to newest, as they existed at the moment the enumerator was created.

Remarks

Enumeration operates on an atomic snapshot captured via ToArray() at the moment this method is called. Any elements enqueued or dequeued after the enumerator is created are not reflected in the enumerated sequence.

Because the enumerator operates on a snapshot, it will never throw InvalidOperationException due to concurrent modification - unlike enumerators on non-concurrent collections.

ICollection.CopyTo(Array, int)

Copies the elements of the ConcurrentCircularBuffer<T> to a one-dimensional, zero-based Array, starting at the specified index.

void ICollection.CopyTo(Array array, int index)

Parameters

array Array

The destination array. Must not be null, must be single-dimensional, and must have zero-based indexing.

index int

The zero-based index in array at which copying begins.

Remarks

This method takes an atomic snapshot of the buffer before copying. The destination array reflects the state of the buffer at the moment the snapshot was taken and is not affected by concurrent modifications made after that point.

This method supports the non-generic ICollection interface and is intended for interop scenarios. For strongly typed copies, use CopyTo(T[], int) instead.

Exceptions

ArgumentNullException

array is null.

ArgumentException

array is multidimensional, does not have zero-based indexing, its element type is incompatible with T, or the number of elements in the buffer exceeds the available space from index to the end of array.

ArgumentOutOfRangeException

index is less than zero.

ICollection.IsSynchronized

Gets a value indicating whether access to the ConcurrentCircularBuffer<T> is synchronized (thread safe).

bool ICollection.IsSynchronized { get; }

Returns

bool

Always false. ConcurrentCircularBuffer<T> manages its own internal synchronization and does not expose a public lock object.

Remarks

Thread safety is achieved through internal lock-free or fine-grained locking mechanisms. Callers should not attempt to coordinate access externally via SyncRoot, as that property is not supported.

ICollection.SyncRoot

Gets an object that can be used to synchronize access to the collection. Not supported on this type - ConcurrentCircularBuffer<T> manages its own internal synchronization.

object ICollection.SyncRoot { get; }

Returns

object

Remarks

Exposing a SyncRoot would allow callers to take the same lock used internally, undermining the concurrency guarantees of the collection. This matches the behavior of ConcurrentQueue<T> and other BCL concurrent collections.

Exceptions

NotSupportedException

Always thrown. Use the thread-safe members of this class directly.

IEnumerable.GetEnumerator()

Returns an enumerator that iterates over a point-in-time snapshot of the buffer's contents.

IEnumerator IEnumerable.GetEnumerator()

Returns

IEnumerator

An ConcurrentCircularBuffer<T>.Enumerator that iterates through the elements in FIFO order, from oldest to newest, as they existed at the moment the enumerator was created.

Remarks

Enumeration operates on an atomic snapshot captured via ToArray() at the moment this method is called. Any elements enqueued or dequeued after the enumerator is created are not reflected in the enumerated sequence.

Because the enumerator operates on a snapshot, it will never throw InvalidOperationException due to concurrent modification - unlike enumerators on non-concurrent collections.

Applies to

ProductVersions
.NET8, 10