xenium
quiescent_state_based.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 QUIESCENT_STATE_BASED_IMPL
7  #error "This is an impl file and must not be included directly!"
8 #endif
9 
10 #include <xenium/detail/port.hpp>
11 #include <xenium/reclamation/detail/orphan.hpp>
12 
13 #include <algorithm>
14 
15 namespace xenium::reclamation {
16 
17 struct quiescent_state_based::thread_control_block : detail::thread_block_list<thread_control_block>::entry {
18  std::atomic<unsigned> local_epoch;
19 };
20 
21 struct quiescent_state_based::thread_data {
22  ~thread_data() {
23  if (control_block == nullptr) {
24  return; // no control_block -> nothing to do
25  }
26 
27  // we can avoid creating an orphan in case we have no retired nodes left.
28  if (std::any_of(retire_lists.begin(), retire_lists.end(), [](auto p) { return p != nullptr; })) {
29  // global_epoch - 1 (mod number_epochs) guarantees a full cycle, making sure no
30  // other thread may still have a reference to an object in one of the retire lists.
31  auto target_epoch = (global_epoch.load(std::memory_order_relaxed) + number_epochs - 1) % number_epochs;
32  assert(target_epoch < number_epochs);
33  global_thread_block_list.abandon_retired_nodes(new detail::orphan<number_epochs>(target_epoch, retire_lists));
34  }
35 
36  global_thread_block_list.release_entry(control_block);
37  control_block = nullptr;
38  }
39 
40  void enter_region() {
41  ensure_has_control_block();
42  ++region_entries;
43  }
44 
45  void leave_region() {
46  if (--region_entries == 0) {
47  quiescent_state();
48  }
49  }
50 
51  void add_retired_node(detail::deletable_object* p) {
52  assert(control_block != nullptr);
53  add_retired_node(p, control_block->local_epoch.load(std::memory_order_relaxed));
54  }
55 
56 private:
57  void ensure_has_control_block() {
58  if (control_block == nullptr) {
59  control_block = global_thread_block_list.acquire_entry();
60  auto epoch = global_epoch.load(std::memory_order_relaxed);
61  do {
62  control_block->local_epoch.store(epoch, std::memory_order_relaxed);
63 
64  // (1) - this acq_rel-CAS synchronizes-with the acquire-load (2)
65  // and the acq_rel-CAS (5)
66  } while (!global_epoch.compare_exchange_weak(epoch, epoch, std::memory_order_acq_rel, std::memory_order_relaxed));
67  }
68  }
69 
70  void quiescent_state() {
71  // (2) - this acquire-load synchronizes-with the acq_rel-CAS (1, 5)
72  auto epoch = global_epoch.load(std::memory_order_acquire);
73 
74  if (control_block->local_epoch.load(std::memory_order_relaxed) == epoch) {
75  const auto new_epoch = (epoch + 1) % number_epochs;
76  if (!try_update_epoch(epoch, new_epoch)) {
77  return;
78  }
79 
80  epoch = new_epoch;
81  }
82 
83  // we either just updated the global_epoch or we are observing a new epoch from some other thread
84  // either way - we can reclaim all the objects from the old 'incarnation' of this epoch
85 
86  // (3) - this release-store synchronizes-with the acquire-fence (4)
87  control_block->local_epoch.store(epoch, std::memory_order_release);
88  detail::delete_objects(retire_lists[epoch]);
89  }
90 
91  void add_retired_node(detail::deletable_object* p, size_t epoch) {
92  assert(epoch < number_epochs);
93  p->next = retire_lists[epoch];
94  retire_lists[epoch] = p;
95  }
96 
97  bool try_update_epoch(unsigned curr_epoch, unsigned new_epoch) {
98  const auto old_epoch = (curr_epoch + number_epochs - 1) % number_epochs;
99  auto prevents_update = [old_epoch](const thread_control_block& data) {
100  // TSan does not support explicit fences, so we cannot rely on the acquire-fence (6)
101  // but have to perform an acquire-load here to avoid false positives.
102  constexpr auto memory_order = TSAN_MEMORY_ORDER(std::memory_order_acquire, std::memory_order_relaxed);
103  return data.local_epoch.load(memory_order) == old_epoch && data.is_active(memory_order);
104  };
105 
106  // If any thread hasn't advanced to the current epoch, abort the attempt.
107  bool cannot_update = std::any_of(global_thread_block_list.begin(), global_thread_block_list.end(), prevents_update);
108  if (cannot_update) {
109  return false;
110  }
111 
112  if (global_epoch.load(std::memory_order_relaxed) == curr_epoch) {
113  // (4) - this acquire-fence synchronizes-with the release-store (3)
114  std::atomic_thread_fence(std::memory_order_acquire);
115 
116  // (5) - this acq_rel-CAS synchronizes-with the acquire-load (2)
117  // and the acq_rel-CAS (1)
118  bool success = global_epoch.compare_exchange_strong(
119  curr_epoch, new_epoch, std::memory_order_acq_rel, std::memory_order_relaxed);
120  if (success) {
121  adopt_orphans();
122  }
123  }
124 
125  // return true regardless of whether the CAS operation was successful or not
126  // it's not import that THIS thread updated the epoch, but it got updated in any case
127  return true;
128  }
129 
130  void adopt_orphans() {
131  auto* cur = global_thread_block_list.adopt_abandoned_retired_nodes();
132  for (detail::deletable_object* next = nullptr; cur != nullptr; cur = next) {
133  next = cur->next;
134  cur->next = nullptr;
135  add_retired_node(cur, static_cast<detail::orphan<number_epochs>*>(cur)->target_epoch);
136  }
137  }
138 
139  unsigned region_entries = 0;
140  thread_control_block* control_block = nullptr;
141  std::array<detail::deletable_object*, number_epochs> retire_lists = {};
142 
143  friend class quiescent_state_based;
144  ALLOCATION_COUNTER(quiescent_state_based);
145 };
146 
147 inline quiescent_state_based::region_guard::region_guard() noexcept {
148  local_thread_data().enter_region();
149 }
150 
151 inline quiescent_state_based::region_guard::~region_guard() noexcept {
152  local_thread_data().leave_region();
153 }
154 
155 template <class T, class MarkedPtr>
156 quiescent_state_based::guard_ptr<T, MarkedPtr>::guard_ptr(const MarkedPtr& p) noexcept : base(p) {
157  if (this->ptr) {
158  local_thread_data().enter_region();
159  }
160 }
161 
162 template <class T, class MarkedPtr>
163 quiescent_state_based::guard_ptr<T, MarkedPtr>::guard_ptr(const guard_ptr& p) noexcept : guard_ptr(MarkedPtr(p)) {}
164 
165 template <class T, class MarkedPtr>
166 quiescent_state_based::guard_ptr<T, MarkedPtr>::guard_ptr(guard_ptr&& p) noexcept : base(p.ptr) {
167  p.ptr.reset();
168 }
169 
170 template <class T, class MarkedPtr>
171 typename quiescent_state_based::template guard_ptr<T, MarkedPtr>&
172  quiescent_state_based::guard_ptr<T, MarkedPtr>::operator=(const guard_ptr& p) noexcept {
173  if (&p == this) {
174  return *this;
175  }
176 
177  reset();
178  this->ptr = p.ptr;
179  if (this->ptr) {
180  local_thread_data().enter_region();
181  }
182 
183  return *this;
184 }
185 
186 template <class T, class MarkedPtr>
187 typename quiescent_state_based::template guard_ptr<T, MarkedPtr>&
188  quiescent_state_based::guard_ptr<T, MarkedPtr>::operator=(guard_ptr&& p) noexcept {
189  if (&p == this) {
190  return *this;
191  }
192 
193  reset();
194  this->ptr = std::move(p.ptr);
195  p.ptr.reset();
196 
197  return *this;
198 }
199 
200 template <class T, class MarkedPtr>
201 void quiescent_state_based::guard_ptr<T, MarkedPtr>::acquire(const concurrent_ptr<T>& p,
202  std::memory_order order) noexcept {
203  if (p.load(std::memory_order_relaxed) == nullptr) {
204  reset();
205  return;
206  }
207 
208  if (!this->ptr) {
209  local_thread_data().enter_region();
210  }
211  // (6) - this load operation potentially synchronizes-with any release operation on p.
212  this->ptr = p.load(order);
213  if (!this->ptr) {
214  local_thread_data().leave_region();
215  }
216 }
217 
218 template <class T, class MarkedPtr>
219 bool quiescent_state_based::guard_ptr<T, MarkedPtr>::acquire_if_equal(const concurrent_ptr<T>& p,
220  const MarkedPtr& expected,
221  std::memory_order order) noexcept {
222  auto actual = p.load(std::memory_order_relaxed);
223  if (actual == nullptr || actual != expected) {
224  reset();
225  return actual == expected;
226  }
227 
228  if (!this->ptr) {
229  local_thread_data().enter_region();
230  }
231  // (7) - this load operation potentially synchronizes-with any release operation on p.
232  this->ptr = p.load(order);
233  if (!this->ptr || this->ptr != expected) {
234  local_thread_data().leave_region();
235  this->ptr.reset();
236  }
237 
238  return this->ptr == expected;
239 }
240 
241 template <class T, class MarkedPtr>
242 void quiescent_state_based::guard_ptr<T, MarkedPtr>::reset() noexcept {
243  if (this->ptr) {
244  local_thread_data().leave_region();
245  }
246  this->ptr.reset();
247 }
248 
249 template <class T, class MarkedPtr>
250 void quiescent_state_based::guard_ptr<T, MarkedPtr>::reclaim(Deleter d) noexcept {
251  this->ptr->set_deleter(std::move(d));
252  local_thread_data().add_retired_node(this->ptr.get());
253  reset();
254 }
255 
256 inline quiescent_state_based::thread_data& quiescent_state_based::local_thread_data() {
257  // workaround for a GCC issue with multiple definitions of __tls_guard
258  static thread_local thread_data local_thread_data;
259  return local_thread_data;
260 }
261 
262 #ifdef TRACK_ALLOCATIONS
263 inline void quiescent_state_based::count_allocation() {
264  local_thread_data().allocation_counter.count_allocation();
265 }
266 
267 inline void quiescent_state_based::count_reclamation() {
268  local_thread_data().allocation_counter.count_reclamation();
269 }
270 #endif
271 } // namespace xenium::reclamation