Skip to content

Real-Time Patterns

Lock-free communication patterns for GUI โ†” Audio thread interaction.

๐ŸŽฏ Patterns Included

1. DoubleBuffer - Settings Updates

Use when: Frequent updates of small data structures (settings, parameters)

Pattern:

Writer (GUI):           Reader (Audio):
1. Get write buffer     1. Get read buffer (wait-free!)
2. Modify               2. Use data
3. Publish (swap)       3. No blocking

Example:

struct Settings {
    float gain;
    float pan;
};

DoubleBuffer<Settings> settings;

// GUI thread
auto& write = settings.getWriteBuffer();
write.gain = 0.5f;
settings.publish();  // Atomic swap

// Audio thread
const auto& read = settings.getReadBuffer();  // Always valid
process(read.gain);

Properties: - โœ… Wait-free reads - โœ… Lock-free writes - โœ… 2x memory overhead - โœ… Single writer, single reader

2. CommandQueue - Discrete Actions

Use when: Infrequent discrete actions (preset load, reset, MIDI events)

Pattern:

Producer (GUI):         Consumer (Audio):
1. Create command       1. Pop all commands
2. Push to queue        2. Execute each
3. Repeat               3. Process audio

Example:

CommandQueue<256> queue;

// GUI thread
queue.push(Command::setParameter(0, 0.5f));
queue.push(Command::loadPreset(42));

// Audio thread
queue.processAll([](const Command& cmd) {
    switch (cmd.type) {
        case Command::Type::SetParameter:
            setParam(cmd.param, cmd.value);
            break;
        case Command::Type::LoadPreset:
            loadPreset(cmd.param);
            break;
    }
});

Properties: - โœ… Lock-free SPSC queue - โœ… Fixed capacity (no allocations) - โœ… Cache-friendly ring buffer - โš ๏ธ Bounded (can be full)

3. RCUPointer - Large Data Updates

Use when: Infrequent updates of large data (impulse responses, lookup tables)

Pattern:

Readers (Audio):        Writer (GUI):
1. Load pointer         1. Clone current data
2. Use data             2. Modify clone
3. Zero cost!           3. Swap pointer
                        4. Delete old (deferred)

Example:

struct IRData {
    std::vector<float> samples;
};

RCUPointer<IRData> ir_data;

// GUI thread
ir_data.update([](IRData& data) {
    // Modify copy
    data.samples = loadNewIR();
});
ir_data.collect();  // Clean up old versions

// Audio thread
const IRData* ir = ir_data.read();  // Just pointer load!
process(ir->samples);

Properties: - โœ… Zero-cost reads (wait-free) - โœ… Writers don't block readers - โš ๏ธ Copy-on-write (expensive for large data) - โš ๏ธ Multiple versions in memory - โš ๏ธ Deferred deletion (grace period)

๐Ÿš€ Usage Guide

When to Use What?

Pattern Update Frequency Data Size Use Case
DoubleBuffer High (>100 Hz) Small Parameter smoothing
CommandQueue Low (<10 Hz) Tiny Preset changes, MIDI
RCUPointer Very low (<1 Hz) Large IR data, lookup tables

Real-World Example

class AudioProcessor {
    // Fast parameter updates
    DoubleBuffer<Settings> settings_;

    // Discrete actions
    CommandQueue<256> commands_;

    // Large data updates
    RCUPointer<IRData> ir_data_;

public:
    // GUI thread
    void setParameter(int id, float value) {
        auto& write = settings_.getWriteBuffer();
        // Update parameter...
        settings_.publish();
    }

    void loadPreset(int id) {
        commands_.push(Command::loadPreset(id));
    }

    void loadIR(const std::vector<float>& samples) {
        ir_data_.update([&](IRData& data) {
            data.samples = samples;
        });
    }

    // Audio thread
    void process(float* buffer, int frames) {
        // 1. Process commands
        commands_.processAll([this](const Command& cmd) {
            handleCommand(cmd);
        });

        // 2. Get latest settings
        const auto& settings = settings_.getReadBuffer();

        // 3. Get IR data (if needed)
        const IRData* ir = ir_data_.read();

        // 4. Process audio
        for (int i = 0; i < frames; ++i) {
            buffer[i] *= settings.gain;
        }
    }
};

๐Ÿงช Testing

mkdir build && cd build
cmake ..
cmake --build .
ctest

Or manually:

cd tests
g++ -std=c++17 -O2 -pthread test_patterns.cpp -o test_patterns
./test_patterns

Test Coverage

  1. โœ… DoubleBuffer (basic + threaded)
  2. โœ… CommandQueue (push/pop, processAll, threaded)
  3. โœ… RCUPointer (basic + threaded + garbage collection)
  4. โœ… Real-world scenario (GUI + Audio threads, all patterns)

๐Ÿ“Š Performance

DoubleBuffer

Read:    ~1 ns (atomic load)
Write:   ~3 ns (atomic swap)
Memory:  2x data size

CommandQueue

Push:    ~10 ns (atomic CAS)
Pop:     ~10 ns (atomic CAS)
Memory:  Fixed (MaxCommands * sizeof(Command))

RCUPointer

Read:    ~1 ns (atomic load) โœจ
Write:   ~1 ฮผs (clone + swap + defer)
Memory:  N versions * data size

โš ๏ธ Common Pitfalls

1. Wrong Pattern Choice

// โŒ BAD: DoubleBuffer for large data
DoubleBuffer<std::vector<float>> huge_data(1'000'000);
// Wastes 2x 1M floats = 8 MB

// โœ… GOOD: RCUPointer for large data
RCUPointer<std::vector<float>> huge_data;
// Only copies when updating (rare)

2. Forgetting to Publish

// โŒ BAD: Forget to publish
auto& write = buffer.getWriteBuffer();
write.gain = 0.5f;
// Reader never sees update!

// โœ… GOOD: Always publish
buffer.publish();

3. Queue Overflow

// โŒ BAD: Ignore push failure
queue.push(cmd);  // Silently fails if full

// โœ… GOOD: Check return value
if (!queue.push(cmd)) {
    // Handle overflow
}

// OR: Use assertion for debugging
queue.pushOrAssert(cmd);

4. RCU Grace Period

// โŒ BAD: Never collect garbage
rcu_ptr.update(new_data);  // Old data never deleted (leak!)

// โœ… GOOD: Collect periodically
rcu_ptr.update(new_data);
rcu_ptr.collect();  // Call from writer thread

๐Ÿ“š References

๐Ÿ“„ License

Part of AudioLab Core library.