xenium
left_right.hpp
1 //
2 // Copyright (c) 2018-2020 Manuel Pöter.
3 // Licensed under the MIT License. See LICENSE file in the project root for full license information.
4 //
5 
6 #ifndef XENIUM_LEFT_RIGHT_HPP
7 #define XENIUM_LEFT_RIGHT_HPP
8 
9 #include <atomic>
10 #include <cassert>
11 #include <mutex>
12 #include <thread>
13 
14 #ifdef _MSC_VER
15  #pragma warning(push)
16  #pragma warning(disable : 4324) // structure was padded due to alignment specifier
17 #endif
18 
19 namespace xenium {
20 
37 template <typename T>
38 struct left_right {
46  explicit left_right(T source) : _left(source), _right(std::move(source)) {}
47 
56  left_right(T left, T right) : _left(std::move(left)), _right(std::move(right)) {}
57 
61  left_right() = default;
62 
76  template <typename Func>
77  auto read(Func&& func) const {
78  read_guard guard(*this);
79  // (1) - this seq-cst-load enforces a total order with the seq-cst-store (2, 3)
80  const T& inst = _lr_indicator.load(std::memory_order_seq_cst) == READ_LEFT ? _left : _right;
81  return func(inst);
82  }
83 
93  template <typename Func>
94  void update(Func&& func) {
95  std::lock_guard<std::mutex> lock(_writer_mutex);
96  assert(_lr_indicator.load() == _version_index.load());
97  if (_lr_indicator.load(std::memory_order_relaxed) == READ_LEFT) {
98  func(_right);
99  // (2) - this seq-cst-store enforces a total order with the seq-cst-load (1)
100  _lr_indicator.store(READ_RIGHT, std::memory_order_seq_cst);
101  toggle_version_and_wait();
102  func(_left);
103  } else {
104  func(_left);
105  // (3) - this seq-cst-store enforces a total order with the seq-cst-load (1)
106  _lr_indicator.store(READ_LEFT, std::memory_order_seq_cst);
107  toggle_version_and_wait();
108  func(_right);
109  }
110  }
111 
112 private:
113  struct alignas(64) read_indicator {
114  void arrive() {
115  // (4) - this seq-cst-fetch-add enforces a total order with the seq-cst-load (6)
116  _counter.fetch_add(1, std::memory_order_seq_cst);
117  }
118  void depart() {
119  // (5) - this release-fetch-sub synchronizes-with the seq-cst-load (6)
120  _counter.fetch_sub(1, std::memory_order_release);
121  // Note: even though this method is only called by reader threads that (usually)
122  // do not change the underlying data structure, we still have to use release
123  // order here to ensure that the read operations is properly ordered before a
124  // subsequent update operation.
125  }
126  bool empty() {
127  // (6) - this seq-cst-load enforces a total order with the seq-cst-fetch-add (4)
128  // and synchronizes-with the release-fetch-add (5)
129  return _counter.load(std::memory_order_seq_cst) == 0;
130  }
131 
132  private:
133  std::atomic<uint64_t> _counter{0};
134  };
135 
136  struct read_guard {
137  explicit read_guard(const left_right& inst) :
138  _indicator(inst.get_read_indicator(inst._version_index.load(std::memory_order_relaxed))) {
139  _indicator.arrive();
140  }
141  ~read_guard() { _indicator.depart(); }
142 
143  private:
144  read_indicator& _indicator;
145  };
146  friend struct read_guard;
147 
148  void toggle_version_and_wait() {
149  const int current_version = _version_index.load(std::memory_order_relaxed);
150  const int current_idx = current_version & 0x1;
151  const int next_idx = (current_version + 1) & 0x1;
152 
153  wait_for_readers(next_idx);
154  _version_index.store(next_idx, std::memory_order_relaxed);
155  wait_for_readers(current_idx);
156  }
157 
158  void wait_for_readers(int idx) {
159  auto& indicator = get_read_indicator(idx);
160  while (!indicator.empty()) {
161  std::this_thread::yield();
162  }
163  }
164 
165  read_indicator& get_read_indicator(int idx) const {
166  assert(idx == 0 || idx == 1);
167  if (idx == 0) {
168  return _read_indicator1;
169  }
170  return _read_indicator2;
171  }
172 
173  static constexpr int READ_LEFT = 0;
174  static constexpr int READ_RIGHT = 1;
175 
176  // TODO: make mutex type configurable via policy
177  std::mutex _writer_mutex;
178  std::atomic<int> _version_index{0};
179  std::atomic<int> _lr_indicator{READ_LEFT};
180 
181  mutable read_indicator _read_indicator1;
182  T _left;
183 
184  mutable read_indicator _read_indicator2;
185  T _right;
186 };
187 } // namespace xenium
188 
189 #ifdef _MSC_VER
190  #pragma warning(pop)
191 #endif
192 
193 #endif
xenium::left_right
Generic implementation of the LeftRight algorithm proposed by Ramalhete and Correia [RC15].
Definition: left_right.hpp:38
xenium::left_right::left_right
left_right(T left, T right)
Initializes the two underlying instances withe the specified sources.
Definition: left_right.hpp:56
xenium::left_right::left_right
left_right(T source)
Initialize the two underlying T instances with the specified source.
Definition: left_right.hpp:46
xenium::left_right::read
auto read(Func &&func) const
Performs a read operation on the active instance using the specified functor.
Definition: left_right.hpp:77
xenium::left_right::update
void update(Func &&func)
Performs an update operation on both underlying instances using the specified functor.
Definition: left_right.hpp:94
xenium::left_right::left_right
left_right()=default
Default constructs both underlying instances.