-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathlock_free_double_buffer.hpp
More file actions
95 lines (79 loc) · 3.42 KB
/
Copy pathlock_free_double_buffer.hpp
File metadata and controls
95 lines (79 loc) · 3.42 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
#pragma once
#include <atomic>
#include <array>
#include <cstddef> // For std::size_t
// Define a macro for cache line alignment to prevent false sharing.
// This is typically 64 bytes on most modern CPUs.
// std::hardware_destructive_interference_size is available since C++17.
#if __cpp_lib_hardware_interference_size >= 201703L
static constexpr std::size_t CACHE_LINE_SIZE = std::hardware_destructive_interference_size;
#else
static constexpr std::size_t CACHE_LINE_SIZE = 64; // Common default
#endif
template <typename T>
class LockFreeDoubleBuffer {
public:
LockFreeDoubleBuffer();
// Producer API
T& get_write_buffer();
void swap_buffers();
// Consumer API
const T& get_read_buffer() const;
bool has_new_data();
private:
// Buffers for data. Aligned to cache line to prevent false sharing.
alignas(CACHE_LINE_SIZE) std::array<T, 2> buffers_;
// Atomic index indicating which buffer is currently being written to by the producer.
// The other buffer is implicitly the one being read from (or waiting to be read).
// Aligned to cache line to prevent false sharing with other atomics/data.
alignas(CACHE_LINE_SIZE) std::atomic<int> write_index_;
// Atomic flag to indicate if new data has been written and is ready for consumption.
// Aligned to cache line to prevent false sharing.
alignas(CACHE_LINE_SIZE) std::atomic<bool> new_data_available_;
};
template <typename T>
LockFreeDoubleBuffer<T>::LockFreeDoubleBuffer()
: write_index_(0), new_data_available_(false)
{
// Default construct the elements in the buffers if T is not a primitive type.
// For primitive types, this does nothing.
// If T has a non-trivial constructor, it will be called here.
}
template <typename T>
T& LockFreeDoubleBuffer<T>::get_write_buffer()
{
// The producer always writes to the buffer indicated by write_index_.
// We use memory_order_relaxed here because the actual synchronization
// happens in swap_buffers and get_read_buffer.
return buffers_[write_index_.load(std::memory_order_relaxed)];
}
template <typename T>
void LockFreeDoubleBuffer<T>::swap_buffers()
{
// Atomically swap the write_index_.
// This makes the currently written buffer available for reading.
// We use memory_order_release to ensure all writes to the buffer
// are visible before the index swap.
int current_write_idx = write_index_.load(std::memory_order_relaxed);
write_index_.store(1 - current_write_idx, std::memory_order_release);
// Set the new_data_available_ flag to true with memory_order_release
// to ensure this write is visible after the index swap.
new_data_available_.store(true, std::memory_order_release);
}
template <typename T>
const T& LockFreeDoubleBuffer<T>::get_read_buffer() const
{
// The consumer reads from the buffer that is NOT currently being written to.
// We use memory_order_acquire to ensure all writes to the buffer
// (made by the producer before calling swap_buffers with memory_order_release)
// are visible before reading the data.
return buffers_[1 - write_index_.load(std::memory_order_acquire)];
}
template <typename T>
bool LockFreeDoubleBuffer<T>::has_new_data()
{
// Check if new data is available. Use memory_order_acquire to ensure
// that if new_data_available_ is true, the data in the read buffer
// is also visible.
return new_data_available_.exchange(false, std::memory_order_acquire);
}