Line data Source code
1 0 : // Distributed under the MIT License. 2 : // See LICENSE.txt for details. 3 : 4 : #pragma once 5 : 6 : #include <atomic> 7 : #include <cstddef> 8 : #include <iterator> 9 : #include <limits> 10 : #include <memory> 11 : #include <mutex> 12 : #include <optional> 13 : 14 : /// \cond 15 : namespace PUP { 16 : class er; 17 : } // namespace PUP 18 : /// \endcond 19 : 20 0 : namespace domain::FunctionsOfTime::FunctionOfTimeHelpers { 21 : namespace ThreadsafeList_detail { 22 : template <typename T> 23 : struct Interval; 24 : } // namespace ThreadsafeList_detail 25 : 26 : /// A list of time intervals that allows safe access to existing 27 : /// elements even during modification. 28 : /// 29 : /// Concurrent modification is not supported. All non-modifying 30 : /// operations except for serialization can be safely performed in 31 : /// parallel with each other and with `insert` and will return a 32 : /// consistent state. 33 : template <typename T> 34 1 : class ThreadsafeList { 35 : private: 36 0 : using Interval = ThreadsafeList_detail::Interval<T>; 37 : 38 : public: 39 : /// No operations are valid on a default-initialized object except 40 : /// for assignment and deserialization. 41 1 : ThreadsafeList(); 42 0 : ThreadsafeList(ThreadsafeList&& other); 43 0 : ThreadsafeList(const ThreadsafeList& other); 44 0 : ThreadsafeList& operator=(ThreadsafeList&& other); 45 0 : ThreadsafeList& operator=(const ThreadsafeList& other); 46 0 : ~ThreadsafeList(); 47 : 48 0 : explicit ThreadsafeList(double initial_time); 49 : 50 : /// Insert data valid over the interval from \p update_time to \p 51 : /// expiration_time. 52 : /// 53 : /// The \p update_time must be the same as the old expiration time. 54 : /// It is passed as a check that the calling code is computing the 55 : /// other arguments with the correct value. 56 1 : void insert(double update_time, T data, double expiration_time); 57 : 58 0 : struct IntervalInfo { 59 0 : double update; 60 0 : const T& data; 61 0 : double expiration; 62 : 63 0 : friend bool operator==(const IntervalInfo& a, const IntervalInfo& b) { 64 : return a.update == b.update and a.data == b.data and 65 : a.expiration == b.expiration; 66 : } 67 0 : friend bool operator!=(const IntervalInfo& a, const IntervalInfo& b) { 68 : return not(a == b); 69 : } 70 : }; 71 : 72 : /// Obtain the start time, data, and expiration time for the time 73 : /// interval containing \p time. 74 : /// 75 : /// If \p time is at an interval boundary, the earlier interval is 76 : /// returned, except for the initial time, which returns the first 77 : /// interval. 78 1 : IntervalInfo operator()(double time) const; 79 : 80 0 : double initial_time() const; 81 : 82 : /// The expiration time of the last data interval. 83 1 : double expiration_time() const; 84 : 85 : /// The first interval boundary after \p time. If \p time is an 86 : /// interval boundary, the one after it is returned. 87 1 : double expiration_after(double time) const; 88 : 89 : /// Remove the oldest data in the list, leaving at least \p length 90 : /// entries. 91 : /// 92 : /// If the list is truncated to zero length, the caller must ensure 93 : /// that no concurrent access or modification is performed. The 94 : /// list is guaranteed to be empty afterwards. 95 : /// 96 : /// Otherwise, it is the caller's responsibility to ensure that, 97 : /// during this call, no reader attempts to look up what will become 98 : /// the new oldest interval. Storing references to that interval's 99 : /// data is safe, and lookups of that interval are safe if they are 100 : /// sequenced after this call. Concurrent insertions are safe, and 101 : /// concurrent access to newer intervals is safe. Concurrent calls 102 : /// to truncate functions and serialization are protected by 103 : /// mutexes, but other concurrent calls are lock-free. 104 : /// 105 : /// If the list is initially shorter than \p length, it is left 106 : /// unchanged. Otherwise, if concurrent insertions occur, the 107 : /// amount of removed data is approximate, but will not leave the 108 : /// list shorter than the requested value. In the absence of 109 : /// concurrent modifications, the list will be left with exactly the 110 : /// requested length. 111 1 : void truncate_to_length(size_t length); 112 : 113 : /// Remove the oldest data in the list, retaining data needed to 114 : /// access at least as far back as \p time. 115 : /// 116 : /// If \p time is before `initial_time()`, the list is not modified. 117 : /// If \p time is after `expiration_time()`, the amount of removed 118 : /// data is unspecified. 119 : /// 120 : /// The amount of removed data is approximate. There may be 121 : /// slightly more data remaining after a call than requested. 122 : /// 123 : /// It is safe to insert and access data concurrently with 124 : /// truncation, as long as the accessed times are not before the 125 : /// truncation time. Concurrent calls to truncate functions and 126 : /// serialization are protected by mutexes, but other concurrent 127 : /// calls are lock-free. 128 1 : void truncate_at_time(double time); 129 : 130 : /// Empty the list. Equivalent to `truncate_to_length(0)`. See 131 : /// that method for details. 132 1 : void clear(); 133 : 134 0 : void pup(PUP::er& p); 135 : 136 0 : class iterator { 137 : public: 138 0 : using iterator_category = std::input_iterator_tag; 139 0 : using value_type = IntervalInfo; 140 0 : using reference = value_type; 141 0 : using pointer = std::optional<value_type>; 142 0 : using difference_type = std::ptrdiff_t; 143 : 144 0 : iterator() = default; 145 0 : iterator& operator++(); 146 0 : iterator operator++(int); 147 0 : reference operator*() const; 148 0 : pointer operator->() const; 149 : 150 0 : friend bool operator==(const iterator& a, const iterator& b) { 151 : return a.interval_ == b.interval_; 152 : } 153 0 : friend bool operator!=(const iterator& a, const iterator& b) { 154 : return not(a == b); 155 : } 156 : 157 : private: 158 0 : friend class ThreadsafeList; 159 : 160 0 : iterator(const ThreadsafeList* parent, const Interval* interval); 161 : 162 0 : const ThreadsafeList* parent_ = nullptr; 163 0 : const Interval* interval_ = nullptr; 164 : }; 165 : 166 : /// Iterate over all the intervals in the list, in reverse order 167 : /// (i.e., most recent first). 168 : /// @{ 169 1 : iterator begin() const; 170 1 : iterator end() const; 171 : /// @} 172 : 173 : private: 174 : // interval_after_boundary controls behavior at boundary points. 175 : // True -> later interval, false -> earlier. Initial time always 176 : // gives the first interval. 177 0 : const Interval& find_interval(double time, 178 : bool interval_after_boundary) const; 179 : 180 0 : alignas(64) std::atomic<double> initial_time_ = 181 : std::numeric_limits<double>::signaling_NaN(); 182 : // Pad memory to avoid false-sharing when accessing initial_time_ 183 : // NOLINTNEXTLINE(modernize-avoid-c-arrays) 184 0 : char unused_padding_initial_time_[64 - (sizeof(initial_time_) % 64)] = {}; 185 0 : std::unique_ptr<Interval> interval_list_{}; 186 0 : alignas(64) std::atomic<Interval*> most_recent_interval_{}; 187 : // Pad memory to avoid false-sharing when accessing most_recent_interval_ 188 : // NOLINTNEXTLINE(modernize-avoid-c-arrays) 189 : char 190 0 : unused_padding_most_recent_interval_[64 - (sizeof(most_recent_interval_) % 191 : 64)] = {}; 192 : // Mutex protecting methods that access the tail of the list, i.e., 193 : // truncation and serialization. Other methods are assumed to be 194 : // called with parameters that will not interact with truncation. 195 0 : std::mutex truncation_mutex_{}; 196 : }; 197 : 198 : template <typename T> 199 0 : bool operator==(const ThreadsafeList<T>& a, const ThreadsafeList<T>& b); 200 : 201 : template <typename T> 202 0 : bool operator!=(const ThreadsafeList<T>& a, const ThreadsafeList<T>& b); 203 : } // namespace domain::FunctionsOfTime::FunctionOfTimeHelpers