userver: userver/concurrent/impl/striped_read_indicator.hpp Source File
⚠️ This is the documentation for an old userver version. Click here to switch to the latest version.
All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Friends Macros Modules Pages Concepts
striped_read_indicator.hpp
1#pragma once
2
3#include <cstdint>
4#include <limits>
5
6#include <userver/concurrent/striped_counter.hpp>
7#include <userver/utils/assert.hpp>
8
9USERVER_NAMESPACE_BEGIN
10
11namespace concurrent::impl {
12
13class StripedReadIndicatorLock;
14
15/// @ingroup userver_concurrency
16///
17/// @brief Protects some data from being modified or deleted as long as there is
18/// at least one reader.
19///
20/// Under heavy concurrent usage, performs far better than something like
21/// a refcount, `Lock-Unlock` is wait-free population oblivious.
22///
23/// Allocates `16 * N_CORES` bytes, which is more than a simple refcount,
24/// of course. Use sparingly and beware of the memory usage.
25///
26/// Another drawback compared to a conventional refcount is that free-ness is
27/// not signaled directly. The only way to await `IsFree` is to check it
28/// periodically.
29///
30/// @see Based on ideas from https://youtu.be/FtaD0maxwec
31class StripedReadIndicator final {
32 public:
33 /// @brief Create a new unused instance of `ReadIndicator`.
34 StripedReadIndicator();
35
36 StripedReadIndicator(StripedReadIndicator&&) = delete;
37 StripedReadIndicator& operator=(StripedReadIndicator&&) = delete;
38 ~StripedReadIndicator();
39
40 /// @brief Mark the indicator as "used" as long as the returned lock is alive.
41 /// @note The lock should not outlive the `StripedReadIndicator`.
42 /// @note The data may still be retired in parallel with a `Lock()` call.
43 /// After calling `Lock`, the reader must check whether the data has been
44 /// retired in the meantime.
45 /// @note `Lock` uses `std::memory_order_relaxed`, and unlock uses
46 /// `std::memory_order_release` to ensure that unlocks don't run ahead
47 /// of locks from `IsFree`'s point of view.
48 /// @warning Readers must ensure that `Lock` is visible by `IsFree` checks
49 /// in other threads when necessary
50 /// (which may require `std::atomic_thread_fence(std::memory_order_seq_cst)`.
51 /// `Lock` and `IsFree` use weak memory orders, which may lead to `Lock`
52 /// unexpectedly not being visible to `IsFree`.
53 StripedReadIndicatorLock Lock() noexcept;
54
55 /// @returns `true` if there are no locks held on the `StripedReadIndicator`.
56 /// @note `IsFree` should only be called after direct access to this
57 /// StripedReadIndicator is closed for readers. Locks acquired during
58 /// the `IsFree` call may or may not be accounted for.
59 /// @note May sometimes falsely return `false` when the `StripedReadIndicator`
60 /// has just become free. Never falsely returns `true`.
61 /// @note Uses effectively `std::memory_order_acquire`.
62 bool IsFree() const noexcept;
63
64 /// @returns `true` if there are no locks held on any of the `indicators`.
65 /// @see IsFree
66 template <typename StripedReadIndicatorRange>
67 static bool AreAllFree(StripedReadIndicatorRange&& indicators) {
68 // See GetActiveCountApprox for implementation strategy explanation.
69 std::uintptr_t released = 0;
70 for (const auto& indicator : indicators) {
71 released += indicator.released_count_.Read();
72 }
73
74 std::uintptr_t acquired = 0;
75 for (const auto& indicator : indicators) {
76 acquired += indicator.acquired_count_.Read();
77 }
78
79 UASSERT(acquired - released <=
80 std::numeric_limits<std::uintptr_t>::max() / 2);
81 return acquired == released;
82 }
83
84 /// Get the total amount of `Lock` calls, useful for metrics.
85 std::uintptr_t GetAcquireCountApprox() const noexcept;
86
87 /// Get the total amount of `Lock` drops, useful for metrics.
88 std::uintptr_t GetReleaseCountApprox() const noexcept;
89
90 /// Get the approximate amount of locks held, useful for metrics.
91 std::uintptr_t GetActiveCountApprox() const noexcept;
92
93 private:
94 friend class StripedReadIndicatorLock;
95
96 void DoLock() noexcept;
97 void DoUnlock() noexcept;
98
99 StripedCounter acquired_count_;
100 StripedCounter released_count_;
101};
102
103/// @brief Keeps the data protected by a StripedReadIndicator from being retired
104class [[nodiscard]] StripedReadIndicatorLock final {
105 public:
106 /// @brief Produces a `null` instance
107 StripedReadIndicatorLock() noexcept = default;
108
109 StripedReadIndicatorLock(StripedReadIndicatorLock&&) noexcept;
110 StripedReadIndicatorLock(const StripedReadIndicatorLock&) noexcept;
111 StripedReadIndicatorLock& operator=(StripedReadIndicatorLock&&) noexcept;
112 StripedReadIndicatorLock& operator=(const StripedReadIndicatorLock&) noexcept;
113 ~StripedReadIndicatorLock();
114
115 private:
116 explicit StripedReadIndicatorLock(StripedReadIndicator& indicator) noexcept;
117
118 friend class StripedReadIndicator;
119
120 StripedReadIndicator* indicator_{nullptr};
121};
122
123} // namespace concurrent::impl
124
125USERVER_NAMESPACE_END