userver: userver/concurrent/impl/striped_read_indicator.hpp Source File
Loading...
Searching...
No Matches
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