userver: userver/rcu/rcu.hpp Source File
Loading...
Searching...
No Matches
rcu.hpp
Go to the documentation of this file.
1#pragma once
2
3/// @file userver/rcu/rcu.hpp
4/// @brief @copybrief rcu::Variable
5
6#include <atomic>
7#include <cstdlib>
8#include <mutex>
9#include <optional>
10#include <utility>
11
12#include <userver/compiler/impl/lifetime.hpp>
13#include <userver/concurrent/impl/asymmetric_fence.hpp>
14#include <userver/concurrent/impl/intrusive_hooks.hpp>
15#include <userver/concurrent/impl/intrusive_stack.hpp>
16#include <userver/concurrent/impl/striped_read_indicator.hpp>
17#include <userver/engine/async.hpp>
18#include <userver/engine/mutex.hpp>
19#include <userver/rcu/fwd.hpp>
20#include <userver/utils/assert.hpp>
21#include <userver/utils/impl/wait_token_storage.hpp>
22
23USERVER_NAMESPACE_BEGIN
24
25/// @brief Read-Copy-Update
26///
27/// @see Based on ideas from
28/// http://www.drdobbs.com/lock-free-data-structures-with-hazard-po/184401890
29/// with modified API
30namespace rcu {
31
32namespace impl {
33
34template <typename T>
35struct SnapshotRecord final {
36 std::optional<T> data;
37 concurrent::impl::StripedReadIndicator indicator;
38 concurrent::impl::SinglyLinkedHook<SnapshotRecord> free_list_hook;
39 SnapshotRecord* next_retired{nullptr};
40};
41
42// Used instead of concurrent::impl::MemberHook to avoid instantiating
43// SnapshotRecord<T> ahead of time.
44template <typename T>
45struct FreeListHookGetter {
46 static auto& GetHook(SnapshotRecord<T>& node) noexcept { return node.free_list_hook; }
47};
48
49template <typename T>
50struct SnapshotRecordFreeList {
51 SnapshotRecordFreeList() = default;
52
53 ~SnapshotRecordFreeList() {
54 list.DisposeUnsafe([](SnapshotRecord<T>& record) { delete &record; });
55 }
56
57 concurrent::impl::IntrusiveStack<SnapshotRecord<T>, FreeListHookGetter<T>> list;
58};
59
60template <typename T>
61class SnapshotRecordRetiredList final {
62public:
63 SnapshotRecordRetiredList() = default;
64
65 bool IsEmpty() const noexcept { return head_ == nullptr; }
66
67 void Push(SnapshotRecord<T>& record) noexcept {
68 record.next_retired = head_;
69 head_ = &record;
70 }
71
72 template <typename Predicate, typename Disposer>
73 void RemoveAndDisposeIf(Predicate predicate, Disposer disposer) {
74 SnapshotRecord<T>** ptr_to_current = &head_;
75
76 while (*ptr_to_current != nullptr) {
77 SnapshotRecord<T>* const current = *ptr_to_current;
78
79 if (predicate(*current)) {
80 *ptr_to_current = std::exchange(current->next_retired, nullptr);
81 disposer(*current);
82 } else {
83 ptr_to_current = &current->next_retired;
84 }
85 }
86 }
87
88private:
89 SnapshotRecord<T>* head_{nullptr};
90};
91
92class ExclusiveMutex final {
93public:
94 void lock() {
95 const bool was_locked = is_locked_.exchange(true);
97 !was_locked,
98 "Detected a race condition when multiple writers Assign to an rcu::Variable concurrently. The value that "
99 "will remain in rcu::Variable when the dust settles is unspecified."
100 );
101 }
102
103 void unlock() noexcept { is_locked_.store(false); }
104
105private:
106 std::atomic<bool> is_locked_{false};
107};
108
109} // namespace impl
110
111/// @brief A handle to the retired object version, which an RCU deleter should
112/// clean up.
113/// @see rcu::DefaultRcuTraits
114template <typename T>
115class SnapshotHandle final {
116public:
117 SnapshotHandle(SnapshotHandle&& other) noexcept
118 : record_(std::exchange(other.record_, nullptr)), free_list_(std::exchange(other.free_list_, nullptr)) {}
119
120 ~SnapshotHandle() {
121 if (record_ != nullptr) {
122 UASSERT(free_list_ != nullptr);
123 record_->data.reset();
124 free_list_->list.Push(*record_);
125 }
126 }
127
128private:
129 template <typename /*T*/, typename Traits>
130 friend class Variable;
131
132 template <typename /*T*/, typename Traits>
133 friend class WritablePtr;
134
135 explicit SnapshotHandle(impl::SnapshotRecord<T>& record, impl::SnapshotRecordFreeList<T>& free_list) noexcept
136 : record_(&record), free_list_(&free_list) {}
137
138 impl::SnapshotRecord<T>* record_;
139 impl::SnapshotRecordFreeList<T>* free_list_;
140};
141
142/// @brief Destroys retired objects synchronously.
143/// @see rcu::DefaultRcuTraits
144struct SyncDeleter final {
145 template <typename T>
146 void Delete(SnapshotHandle<T>&& handle) noexcept {
147 [[maybe_unused]] const auto for_deletion = std::move(handle);
148 }
149};
150
151/// @brief Destroys retired objects asynchronously in the same `TaskProcessor`.
152/// @see rcu::DefaultRcuTraits
153class AsyncDeleter final {
154public:
155 ~AsyncDeleter() { wait_token_storage_.WaitForAllTokens(); }
156
157 template <typename T>
158 void Delete(SnapshotHandle<T>&& handle) noexcept {
159 if constexpr (std::is_trivially_destructible_v<T> || std::is_same_v<T, std::string>) {
160 SyncDeleter{}.Delete(std::move(handle));
161 } else {
162 try {
163 engine::DetachUnscopedUnsafe(engine::CriticalAsyncNoTracing(
164 // The order of captures is important, 'handle' must be destroyed before 'token'.
165 [token = wait_token_storage_.GetToken(), handle = std::move(handle)]() mutable {}
166 ));
167 // NOLINTNEXTLINE(bugprone-empty-catch)
168 } catch (...) {
169 // Task creation somehow failed.
170 // `handle` will be destroyed synchronously, because it is already moved
171 // into the task's lambda.
172 }
173 }
174 }
175
176private:
177 utils::impl::WaitTokenStorage wait_token_storage_;
178};
179
180/// @brief Default RCU traits. Deletes garbage asynchronously.
181/// Designed for storing data of multi-megabyte or multi-gigabyte caches.
182/// @note Allows reads from any kind of thread.
183/// Only allows writes from coroutine threads.
184/// @see rcu::Variable
185/// @see rcu::SyncRcuTraits
186/// @see rcu::BlockingRcuTraits
188 /// `MutexType` is a writer's mutex type that has to be used to protect
189 /// structure on update.
190 using MutexType = engine::Mutex;
191
192 /// `DeleterType` is used to delete retired objects. It should:
193 /// 1. should contain `void Delete(SnapshotHandle<T>) noexcept`;
194 /// 2. force synchronous cleanup of remaining handles on destruction.
195 using DeleterType = AsyncDeleter;
196};
197
198/// @brief Deletes garbage synchronously.
199/// Designed for storing small amounts of data with relatively fast destructors.
200/// @note Allows reads from any kind of thread.
201/// Only allows writes from coroutine threads.
202/// @see rcu::DefaultRcuTraits
204 using DeleterType = SyncDeleter;
205};
206
207/// @brief Rcu traits for using outside of coroutines.
208/// @note Allows reads from any kind of thread.
209/// Only allows writes from NON-coroutine threads.
210/// @warning Blocks writing threads which are coroutines, which can cause
211/// deadlocks and hangups.
212/// @see rcu::DefaultRcuTraits
214 using MutexType = std::mutex;
215 using DeleterType = SyncDeleter;
216};
217
218/// @brief Rcu traits that only allow a single writer.
219/// Detects race conditions when multiple writers call `Assign` concurrently.
220/// @note Allows reads and writes from any kind of thread.
221/// @see rcu::DefaultRcuTraits
223 using MutexType = impl::ExclusiveMutex;
224 using DeleterType = SyncDeleter;
225};
226
227/// Reader smart pointer for rcu::Variable<T>. You may use operator*() or
228/// operator->() to do something with the stored value. Once created,
229/// ReadablePtr references the same immutable value: if Variable's value is
230/// changed during ReadablePtr lifetime, it will not affect value referenced by
231/// ReadablePtr.
232template <typename T, typename RcuTraits>
233class [[nodiscard]] ReadablePtr final {
234public:
235 explicit ReadablePtr(const Variable<T, RcuTraits>& ptr) {
236 auto* record = ptr.current_.load();
237
238 while (true) {
239 // Lock 'record', which may or may not be 'current_' by the time we got
240 // there.
241 lock_ = record->indicator.GetLock();
242
243 // seq_cst is required for indicator.Lock in the following case.
244 //
245 // Reader thread point-of-view:
246 // 1. [reader] load current_
247 // 2. [reader] indicator.Lock
248 // 3. [reader] load current_
249 // 4. [writer] store current_
250 // 5. [writer] indicator.IsFree
251 //
252 // Given seq_cst only on (3), (4), and (5), the writer can see
253 // (2) after (5). In this case the reader will think that it has
254 // successfully taken the lock, which is false.
255 //
256 // So we need seq_cst on all of (2), (3), (4), (5). Making (2) seq_cst is
257 // somewhat expensive, but this is a well-known cost of hazard pointers.
258 //
259 // The seq_cst cost can be mitigated by utilizing asymmetric fences.
260 // This asymmetric fence effectively grants std::memory_order_seq_cst
261 // to indicator.Lock when applied together with AsymmetricThreadFenceHeavy
262 // in (5). The technique is taken from Folly HazPtr.
263 concurrent::impl::AsymmetricThreadFenceLight();
264
265 // Is the record we locked 'current_'? If so, congratulations, we are
266 // holding a lock to 'current_'.
267 auto* new_current = ptr.current_.load(std::memory_order_seq_cst);
268 if (new_current == record) {
269 break;
270 }
271
272 // 'current_' changed, try again
273 record = new_current;
274 }
275
276 ptr_ = &*record->data;
277 }
278
279 ReadablePtr(ReadablePtr&& other) noexcept = default;
280 ReadablePtr& operator=(ReadablePtr&& other) noexcept = default;
281 ReadablePtr(const ReadablePtr& other) = default;
282 ReadablePtr& operator=(const ReadablePtr& other) = default;
283 ~ReadablePtr() = default;
284
285 const T* Get() const& USERVER_IMPL_LIFETIME_BOUND {
286 UASSERT(ptr_);
287 return ptr_;
288 }
289
290 const T* Get() && { return GetOnRvalue(); }
291
292 const T* operator->() const& USERVER_IMPL_LIFETIME_BOUND { return Get(); }
293 const T* operator->() && { return GetOnRvalue(); }
294
295 const T& operator*() const& USERVER_IMPL_LIFETIME_BOUND { return *Get(); }
296 const T& operator*() && { return *GetOnRvalue(); }
297
298private:
299 const T* GetOnRvalue() {
300 static_assert(!sizeof(T), "Don't use temporary ReadablePtr, store it to a variable");
301 std::abort();
302 }
303
304 const T* ptr_;
305 concurrent::impl::StripedReadIndicatorLock lock_;
306};
307
308/// Smart pointer for rcu::Variable<T> for changing RCU value. It stores a
309/// reference to a to-be-changed value and allows one to mutate the value (e.g.
310/// add items to std::unordered_map). Changed value is not visible to readers
311/// until explicit store by Commit.
312///
313/// Only a single writer may own a WritablePtr associated with the same
314/// Variable. With regular mutex traits, the writer mutex serializes concurrent
315/// write transactions and is held until Commit or WritablePtr destruction.
316/// StartWrite acquires the mutex and then copies the latest committed value into
317/// the transaction. In-place write APIs acquire the mutex before constructing
318/// the internal replacement value. This critical section doesn't affect
319/// readers, so a slow writer doesn't block readers.
320/// @note you may not pass WritablePtr between coroutines as it owns
321/// engine::Mutex, which must be unlocked in the same coroutine that was used to
322/// lock the mutex.
323template <typename T, typename RcuTraits>
324class [[nodiscard]] WritablePtr final {
325public:
326 /// @cond
327 // For internal use only. Use `var.StartWrite()` instead
328 explicit WritablePtr(Variable<T, RcuTraits>& var)
329 : var_(var),
330 lock_(var.mutex_),
331 record_(&var.EmplaceSnapshot(*var.current_.load()->data))
332 {}
333
334 // For internal use only. Use `var.Emplace(args...)` instead
335 template <typename... Args>
336 WritablePtr(Variable<T, RcuTraits>& var, std::in_place_t, Args&&... initial_value_args)
337 : var_(var),
338 lock_(var.mutex_),
339 record_(&var.EmplaceSnapshot(std::forward<Args>(initial_value_args)...))
340 {}
341 /// @endcond
342
343 WritablePtr(WritablePtr&& other) noexcept
344 : var_(other.var_), lock_(std::move(other.lock_)), record_(std::exchange(other.record_, nullptr)) {}
345
346 ~WritablePtr() {
347 if (record_) {
348 var_.DeleteSnapshot(*record_);
349 }
350 }
351
352 /// Store the changed value in Variable and release the writer mutex. After
353 /// Commit() the value becomes visible to new readers (IOW, Variable::Read()
354 /// returns ReadablePtr referencing the stored value, not an old value).
355 void Commit() {
356 UASSERT(record_ != nullptr);
357 var_.DoAssign(*std::exchange(record_, nullptr), lock_);
358 lock_.unlock();
359 }
360
361 T* Get() & USERVER_IMPL_LIFETIME_BOUND {
362 UASSERT(record_ != nullptr);
363 return &*record_->data;
364 }
365
366 T* Get() && { return GetOnRvalue(); }
367
368 T* operator->() & USERVER_IMPL_LIFETIME_BOUND { return Get(); }
369 T* operator->() && { return GetOnRvalue(); }
370
371 T& operator*() & USERVER_IMPL_LIFETIME_BOUND { return *Get(); }
372 T& operator*() && { return *GetOnRvalue(); }
373
374private:
375 [[noreturn]] static T* GetOnRvalue() {
376 static_assert(!sizeof(T), "Don't use temporary WritablePtr, store it to a variable");
377 std::abort();
378 }
379
380 Variable<T, RcuTraits>& var_;
381 std::unique_lock<typename RcuTraits::MutexType> lock_;
382 impl::SnapshotRecord<T>* record_;
383};
384
385/// @ingroup userver_concurrency userver_containers
386///
387/// @brief Read-Copy-Update variable
388///
389/// @see Based on ideas from
390/// http://www.drdobbs.com/lock-free-data-structures-with-hazard-po/184401890
391/// with modified API.
392///
393/// A variable with MT-access pattern "very often reads, seldom writes". It is
394/// specially optimized for reads. On read, one obtains a ReaderPtr<T> from it
395/// and uses the obtained value as long as it wants to. On write, one obtains a
396/// WritablePtr<T> with a copy of the last version of the value, makes some
397/// changes to it, and commits the result to update current variable value (does
398/// Read-Copy-Update). Old version of the value is not freed on update, it will
399/// be eventually freed when a subsequent writer identifies that nobody works
400/// with this version.
401///
402/// Write transactions on the same @ref rcu::Variable are mutually exclusive.
403/// With @ref rcu::DefaultRcuTraits, @ref rcu::SyncRcuTraits, and
404/// @ref rcu::BlockingRcuTraits, concurrent writers wait and proceed one by one.
405/// @ref rcu::Variable::StartWrite first acquires the writer mutex and then
406/// copies the latest committed value into its transaction, so a writer observes
407/// changes committed by preceding writers. The mutex is held until
408/// @ref rcu::WritablePtr::Commit or @ref rcu::WritablePtr destruction. The
409/// order in which concurrent writers acquire the mutex is unspecified. Readers
410/// don't acquire the writer mutex and may continue using older snapshots while
411/// a writer is active.
412///
413/// @note @ref rcu::ExclusiveRcuTraits requires the caller to guarantee that
414/// write operations on the same @ref rcu::Variable never overlap. If another
415/// writer starts while a write transaction is active, an invariant violation
416/// is reported instead of waiting: debug builds abort, while release builds
417/// throw @ref utils::InvariantError.
418///
419/// @note The writer mutex protects copying, construction, and publication of
420/// the internal RCU snapshot. It does not protect evaluation or copying of
421/// arguments before a write method is entered.
422///
423/// @note There is no way to create a "null" `Variable`.
424///
425/// ## Example usage:
426///
427/// @snippet core/src/rcu/rcu_test.cpp Sample rcu::Variable usage
428///
429/// @see @ref scripts/docs/en/userver/synchronization.md
430///
431/// @tparam T the stored value
432/// @tparam RcuTraits traits, should inherit from rcu::DefaultRcuTraits
433template <typename T, typename RcuTraits>
434class Variable final {
435 static_assert(
436 std::is_base_of_v<DefaultRcuTraits, RcuTraits>,
437 "RcuTraits should publicly inherit from rcu::DefaultRcuTraits"
438 );
439
440public:
441 using MutexType = typename RcuTraits::MutexType;
442 using DeleterType = typename RcuTraits::DeleterType;
443
444 /// @brief Create a new `Variable` with an in-place constructed initial value.
445 /// @param initial_value_args arguments passed to the constructor of the
446 /// initial value
447 template <typename... Args>
448 // TODO make explicit
449 Variable(Args&&... initial_value_args)
450 : current_(&EmplaceSnapshot(std::forward<Args>(initial_value_args)...))
451 {}
452
453 Variable(const Variable&) = delete;
454 Variable(Variable&&) = delete;
455 Variable& operator=(const Variable&) = delete;
456 Variable& operator=(Variable&&) = delete;
457
458 ~Variable() {
459 {
460 auto* record = current_.load();
461 UASSERT_MSG(record->indicator.IsFree(), "RCU variable is destroyed while being used");
462 delete record;
463 }
464
465 retired_list_.RemoveAndDisposeIf(
466 [](impl::SnapshotRecord<T>&) { return true; },
467 [](impl::SnapshotRecord<T>& record) {
468 UASSERT_MSG(record.indicator.IsFree(), "RCU variable is destroyed while being used");
469 delete &record;
470 }
471 );
472 }
473
474 /// Obtain a smart pointer which can be used to read the current value.
475 ReadablePtr<T, RcuTraits> Read() const { return ReadablePtr<T, RcuTraits>(*this); }
476
477 /// Obtain a copy of contained value.
478 T ReadCopy() const {
479 auto ptr = Read();
480 return *ptr;
481 }
482
483 /// Obtain a smart pointer that will *copy* the current value. First acquires
484 /// the writer mutex, then copies the latest committed value into the
485 /// transaction. The pointer can be used to make changes to the value and to
486 /// set the `Variable` to the changed value. It owns the writer mutex until
487 /// Commit or destruction.
488 WritablePtr<T, RcuTraits> StartWrite() { return WritablePtr<T, RcuTraits>(*this); }
489
490 /// Obtain a smart pointer to a newly in-place constructed value, but does
491 /// not replace the current one yet (in contrast with regular `Emplace`).
492 /// First acquires the writer mutex, then constructs the internal replacement
493 /// value. Owns the mutex until Commit or destruction. Function arguments are
494 /// evaluated before the call and are not protected by this mutex.
495 template <typename... Args>
496 WritablePtr<T, RcuTraits> StartWriteEmplace(Args&&... args) {
497 return WritablePtr<T, RcuTraits>(*this, std::in_place, std::forward<Args>(args)...);
498 }
499
500 /// Replaces the `Variable`'s value with the provided one. Construction of
501 /// the `new_value` parameter happens before the writer mutex is acquired;
502 /// moving it into the internal RCU snapshot and publishing it are serialized
503 /// with other write operations.
504 void Assign(T new_value) { WritablePtr<T, RcuTraits>(*this, std::in_place, std::move(new_value)).Commit(); }
505
506 /// Replaces the `Variable`'s value with an in-place constructed one. Function
507 /// arguments are evaluated before the call; construction of the internal RCU
508 /// snapshot and its publication are serialized with other write operations.
509 template <typename... Args>
510 void Emplace(Args&&... args) {
511 WritablePtr<T, RcuTraits>(*this, std::in_place, std::forward<Args>(args)...).Commit();
512 }
513
514 void Cleanup() {
515 std::unique_lock lock(mutex_, std::try_to_lock);
516 if (!lock.owns_lock()) {
517 // Someone is already assigning to the RCU. They will call ScanRetireList
518 // in the process.
519 return;
520 }
521 ScanRetiredList(lock);
522 }
523
524private:
525 friend class ReadablePtr<T, RcuTraits>;
526 friend class WritablePtr<T, RcuTraits>;
527
528 void DoAssign(impl::SnapshotRecord<T>& new_snapshot, std::unique_lock<MutexType>& lock) {
529 UASSERT(lock.owns_lock());
530
531 // Note: exchange RMW operation would not give any benefits here.
532 auto* const old_snapshot = current_.load();
533 current_.store(&new_snapshot, std::memory_order_seq_cst);
534
535 UASSERT(old_snapshot);
536 retired_list_.Push(*old_snapshot);
537 ScanRetiredList(lock);
538 }
539
540 template <typename... Args>
541 [[nodiscard]] impl::SnapshotRecord<T>& EmplaceSnapshot(Args&&... args) {
542 auto* const free_list_record = free_list_.list.TryPop();
543 auto& record = free_list_record ? *free_list_record : *new impl::SnapshotRecord<T>{};
544 UASSERT(!record.data);
545
546 try {
547 record.data.emplace(std::forward<Args>(args)...);
548 } catch (...) {
549 free_list_.list.Push(record);
550 throw;
551 }
552
553 return record;
554 }
555
556 void ScanRetiredList(std::unique_lock<MutexType>& lock) noexcept {
557 UASSERT(lock.owns_lock());
558 if (retired_list_.IsEmpty()) {
559 return;
560 }
561
562 concurrent::impl::AsymmetricThreadFenceHeavy();
563
564 retired_list_.RemoveAndDisposeIf(
565 [](impl::SnapshotRecord<T>& record) { return record.indicator.IsFree(); },
566 [&](impl::SnapshotRecord<T>& record) { DeleteSnapshot(record); }
567 );
568 }
569
570 void DeleteSnapshot(impl::SnapshotRecord<T>& record) noexcept {
571 static_assert(
572 noexcept(deleter_.Delete(SnapshotHandle<T>{record, free_list_})),
573 "DeleterType::Delete must be noexcept"
574 );
575 deleter_.Delete(SnapshotHandle<T>{record, free_list_});
576 }
577
578 // Covers current_ writes, free_list_.Pop, retired_list_
579 MutexType mutex_{};
580 impl::SnapshotRecordFreeList<T> free_list_;
581 impl::SnapshotRecordRetiredList<T> retired_list_;
582 // Must be placed after 'free_list_' to force sync cleanup before
583 // the destruction of free_list_.
584 DeleterType deleter_{};
585 // Must be placed after 'free_list_' and 'deleter_' so that if
586 // the initialization of current_ throws, it can be disposed properly.
587 std::atomic<impl::SnapshotRecord<T>*> current_;
588};
589
590} // namespace rcu
591
592USERVER_NAMESPACE_END