Double Buffer System
Overview
The DAO Double Buffer system provides a thread-safe mechanism for sharing data between threads with minimal locking. It implements a classic double buffering pattern where one buffer is used for reading while the other is used for writing, with the ability to swap buffers atomically.
This pattern is particularly useful in real-time applications where consistent access to data is required without blocking writers or readers.
Key Features
Thread-safe access to shared data
Zero-copy data transfer
NUMA-aware memory allocation
Support for multiple data types
Buffer swapping with frame synchronization
Low-latency access to current data
DoubleBuffer Class
The DoubleBuffer class is a template class that can store any data type:
template <class T> class DoubleBuffer {
// ...
};
Construction
The constructor initializes the double buffer with a specified number of elements and an optional initial value. It can also allocate memory on a specific NUMA node. The double buffer is templated, allowing it to store any data type. The constructor allocates memory for two buffers of the specified size and initializes them with the given fill value.
The constructor signature is as follows:
DoubleBuffer(size_t numberOfElements, int alloc_now_node = -1, T fillvalue = 0)
Parameters:
numberOfElements: Number of elements the buffer will store
alloc_now_node: NUMA node to allocate memory on (-1 for default allocation)
fillvalue: Initial value for all elements in both buffers (default is 0)
Buffer Management
The DoubleBuffer class maintains two internal buffers and tracks which one is currently active:
Active Buffer: Used for reading, represents current state
Passive Buffer: Used for writing, represents future state
Buffer States
Active Index: Index of the currently active buffer (0 or 1)
Dirty Flag: Indicates if the passive buffer has been modified
Target Frame: Optional frame number for synchronized swapping
Core Methods
The DoubleBuffer class provides several core methods for managing the buffers:
Buffer Access
Active(): Get pointer to the active buffer for reading
Passive(): Get pointer to the passive buffer for writing
SetActiveBuffer(int index): Manually set which buffer is active
SwapBuffers(): Switch the active and passive buffers
Data Operations
CopyIn(T* data, uint64_t frame = 0): Copy data into the passive buffer
CopyAndSwap(T* data): Copy data into passive buffer and immediately swap
The CopyAndSwap is designed to be overloaded incase data manipulation is required on loading the data into the double buffer.
NUMA Integration
AllocOnNode(): Allocate buffer memory on a specific NUMA node
GetNode(): Get the NUMA node where buffers are allocated
Frame Synchronization
Active(uint64_t &frame): Get active buffer, swap if target frame is reached
SetDirty(): Mark the passive buffer as modified
GetDirty(): Check if the passive buffer is modified
Usage Patterns
Basic Usage
// Create a double buffer for 1024 float values
Dao::DoubleBuffer<float> buffer(1024);
// Get pointer to active buffer for reading
float* data = buffer.Active();
// Process data
processData(data, 1024);
// Later, update data
float* newData = generateNewData();
buffer.CopyAndSwap(newData);
NUMA-Aware Usage
// Create a double buffer on NUMA node 0 with initial value 0.0
Dao::DoubleBuffer<float> buffer(1024, 0, 0.0);
// Verify NUMA node
int node = buffer.GetNode(); // Should return 0
Frame Synchronized Updates
// Update buffer with new data and a frame number
buffer.CopyIn(newData, frameCounter);
// Later, access buffer with frame check
uint64_t currentFrame = frameCounter;
float* data = buffer.Active(currentFrame); // Will swap if frameCounter >= target
Low-Level Buffer Management
// Manually manage buffers
buffer.SetActiveBuffer(0); // Set buffer 0 as active
// Get passive buffer for writing
float* writeBuffer = buffer.Passive();
// Modify write buffer directly
for (int i = 0; i < 1024; i++) {
writeBuffer[i] = i * 0.1f;
}
// Mark as dirty and swap
buffer.SetDirty();
buffer.SwapBuffers();
Best Practices
Memory Management: Be aware of buffer allocation and deallocation, especially with NUMA
Thread Safety: The double buffer itself is not thread-safe; external synchronization is required for multithreaded access
Buffer Size: Choose appropriate buffer size to balance memory usage and performance
Frame Synchronization: Use frame numbers for synchronized updates in frame-based applications
Error Handling: Check for allocation failures when creating double buffers
Integration with Component System
The DoubleBuffer class is designed to work seamlessly with the DAO Component system:
// In component constructor
m_dataBuffer = new DoubleBuffer<float>(1024, m_node);
// In component update thread
void ComponentUpdateThread::RestartableThread()
{
// Check for new data
if (m_shm->get_counter() > m_counter)
{
// Copy new data to passive buffer and swap
m_dataBuffer->CopyAndSwap(m_shm->get_frame());
m_counter = m_shm->get_counter();
}
}
// In processing logic
float* currentData = m_dataBuffer->Active();
processData(currentData);
Example: Real-Time Data Processing
#include <daoDoubleBuffer.hpp>
class DataProcessor
{
public:
DataProcessor(int bufferSize)
: m_buffer(bufferSize)
{
// Initialize
}
void updateData(float* newData)
{
// Copy new data and swap buffers
m_buffer.CopyAndSwap(newData);
}
void processData()
{
// Get current data without blocking
float* data = m_buffer.Active();
// Process data without worrying about concurrent updates
for (size_t i = 0; i < m_buffer.Size(); i++)
{
// Process each element
result[i] = processElement(data[i]);
}
}
private:
Dao::DoubleBuffer<float> m_buffer;
};