Course contentsShow
← Rust
Lesson 1229 of 1,91010. Concurrency PrimitivesFree lesson

Building a Zero-Allocation MPSC Queue

You will design a custom queue optimized for extreme high-frequency message passing.

In this lesson, you will learn how to architect a Multi-Producer, Single-Consumer (MPSC) queue that eliminates runtime heap allocations by using pre-allocated static memory.

The Problem with Traditional Queues

Standard queues often use VecDeque or Box pointers, which trigger heap allocations whenever the queue needs to resize or grow. In high-frequency systems, these allocations are "performance killers" because they invoke the system allocator, leading to non-deterministic latency spikes. To achieve zero-allocation, we must instead use a fixed-size, pre-allocated buffer where the producers and consumers communicate solely through atomic indices.

The Circular Buffer Strategy

A zero-allocation MPSC queue relies on a circular array (a ring buffer) of fixed capacity. We use std::sync::atomic types to track the "head" (where the consumer reads) and the "tail" (where producers write).

Because we have multiple producers, we cannot simply increment a single atomic tail. Instead, we use compare_exchange to reserve a slot in the buffer:

use std::sync::atomic::{AtomicUsize, Ordering};

struct MpscQueue<T, const N: usize> {
    buffer: [T; N],
    head: AtomicUsize,
    tail: AtomicUsize,
}

// Simplified slot reservation
fn try_push<T>(queue: &MpscQueue<T, N>, val: T) -> bool {
    let current_tail = queue.tail.load(Ordering::Relaxed);
    // Attempt to claim a slot by incrementing tail atomically
    if queue.tail.compare_exchange(current_tail, current_tail + 1, 
        Ordering::SeqCst, Ordering::Relaxed).is_ok() {
        // Write to the slot (requires UnsafeCell for interior mutability)
        return true;
    }
    false
}

Eliminating Allocations

By using a const N generic parameter, the buffer is allocated on the stack (or as part of a static struct) when the queue is initialized. There is no malloc involved during the lifecycle of the queue. The producers perform a "claim" on the next available index, and the consumer simply polls the head index. If the queue is full or empty, we return a variant rather than growing the structure, shifting the responsibility of backpressure to the application layer.

Key Takeaway: By pre-allocating a fixed-capacity ring buffer and using atomic index reservation, you can bypass the heap entirely, ensuring consistent, high-frequency message passing performance.