Skip to content

Commit a397fc1

Browse files
authored
Merge pull request #539 from beached/v2
Add atomic wait utilities and corresponding tests
2 parents 6005205 + 306e84a commit a397fc1

File tree

10 files changed

+722
-12
lines changed

10 files changed

+722
-12
lines changed

CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
cmake_minimum_required( VERSION 3.14 )
1010

1111
project( "daw-header-libraries"
12-
VERSION "2.115.1"
12+
VERSION "2.118.0"
1313
DESCRIPTION "Various headers"
1414
HOMEPAGE_URL "https://github.com/beached/header_libraries"
1515
LANGUAGES C CXX

include/daw/daw_atomic_wait.h

Lines changed: 370 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,370 @@
1+
// Copyright (c) Darrell Wright
2+
//
3+
// Distributed under the Boost Software License, Version 1.0. (See accompanying
4+
// file LICENSE or copy at http://www.boost.org/LICENSE_1_0.txt)
5+
//
6+
// Official repository: https://github.com/beached/header_libraries
7+
//
8+
9+
#pragma once
10+
11+
#include "daw_attributes.h"
12+
#include "daw_concepts.h"
13+
14+
#include <atomic>
15+
#include <chrono>
16+
#include <thread>
17+
#include <utility>
18+
19+
namespace daw {
20+
enum class wait_status { found, timeout };
21+
}
22+
23+
namespace daw::atomic_impl {
24+
inline constexpr struct {
25+
DAW_ATTRIB_INLINE DAW_CPP23_STATIC_CALL_OP bool operator( )(
26+
std::chrono::nanoseconds elapsed ) DAW_CPP23_STATIC_CALL_OP_CONST {
27+
if( elapsed > std::chrono::milliseconds( 128 ) ) {
28+
std::this_thread::sleep_for( std::chrono::milliseconds( 8 ) );
29+
} else if( elapsed > std::chrono::microseconds( 64 ) ) {
30+
std::this_thread::sleep_for( elapsed / 2 );
31+
} else if( elapsed > std::chrono::microseconds( 4 ) ) {
32+
std::this_thread::yield( );
33+
} else {
34+
// poll
35+
}
36+
return false;
37+
}
38+
} timed_backoff_policy{ };
39+
40+
[[nodiscard]] bool
41+
poll_with_backoff( Fn<bool( std::chrono::nanoseconds )> auto &&backoff_policy,
42+
Fn<bool( )> auto &&func,
43+
std::chrono::nanoseconds max_elapsed ) {
44+
45+
auto const start_time = std::chrono::high_resolution_clock::now( );
46+
int count = 0;
47+
while( true ) {
48+
if( func( ) ) {
49+
return true;
50+
}
51+
if( count < 64 ) {
52+
++count;
53+
continue;
54+
}
55+
auto const elapsed =
56+
std::chrono::high_resolution_clock::now( ) - start_time;
57+
if( elapsed >= max_elapsed ) {
58+
return false;
59+
}
60+
if( backoff_policy( elapsed ) ) {
61+
return false;
62+
}
63+
}
64+
}
65+
} // namespace daw::atomic_impl
66+
67+
namespace daw {
68+
69+
template<typename T, typename Rep, typename Period>
70+
/**
71+
* Waits for the atomic object to change from the old value or until the
72+
* timeout expires.
73+
*
74+
* This function blocks the calling thread, periodically polling the atomic
75+
* object, and returns as soon as the atomic value differs from the specified
76+
* old value or the relative timeout period elapses.
77+
*
78+
* @tparam T The type of the atomic object.
79+
* @tparam Rep The representation type of the relative time duration.
80+
* @tparam Period The tick period type of the relative time duration.
81+
*
82+
* @param object Pointer to the atomic object to wait on.
83+
* @param old The value that the atomic object is expected to differ from.
84+
* @param rel_time The maximum duration to wait before timing out.
85+
* @param order The memory order to use for atomic load operations. Defaults
86+
* to `std::memory_order_acquire`.
87+
*
88+
* @return A wait_status indicating whether the atomic value changed or the
89+
* timeout expired.
90+
*/
91+
[[nodiscard]] wait_status
92+
atomic_wait_for( std::atomic<T> const *object, T const &old,
93+
std::chrono::duration<Rep, Period> const &rel_time,
94+
std::memory_order order = std::memory_order_acquire ) {
95+
auto const final_time =
96+
std::chrono::high_resolution_clock::now( ) + rel_time;
97+
auto current =
98+
std::atomic_load_explicit( object, std::memory_order_acquire );
99+
100+
for( int tries = 0; current == old and tries < 16; ++tries ) {
101+
std::this_thread::yield( );
102+
current = std::atomic_load_explicit( object, order );
103+
}
104+
auto const poll_fn = [&] {
105+
current = std::atomic_load_explicit( object, order );
106+
if( current != old ) {
107+
return true;
108+
}
109+
auto const current_time = std::chrono::high_resolution_clock::now( );
110+
auto const is_timed_out = current_time >= final_time;
111+
return is_timed_out;
112+
};
113+
(void)atomic_impl::poll_with_backoff( atomic_impl::timed_backoff_policy,
114+
poll_fn, rel_time );
115+
if( current == old ) {
116+
return wait_status::timeout;
117+
}
118+
return wait_status::found;
119+
}
120+
121+
/**
122+
* Waits until the atomic object changes from the specified value or the
123+
* timeout time is reached.
124+
*
125+
* This function blocks the calling thread until the atomic value
126+
* pointed to by `object` is not equal to `old` or until the specified
127+
* `timeout_time` is reached.
128+
*
129+
* @tparam T The type of the atomic object.
130+
* @tparam Clock The clock type used to measure the timeout.
131+
* @tparam Duration The duration type used to represent the timeout.
132+
*
133+
* @param object Pointer to the atomic object to wait on.
134+
* @param old The value that the atomic object is expected to change from
135+
* @param timeout_time The time point at which the wait is aborted if the
136+
* value has not been reached.
137+
* @param order The memory order to use. Defaults to
138+
* `std::memory_order_acquire`.
139+
*
140+
* @return A `wait_status` indicating whether the wait was successful or if it
141+
* timed out.
142+
*/
143+
template<typename T, typename Clock, typename Duration>
144+
[[nodiscard]] wait_status atomic_wait_until(
145+
std::atomic<T> const *object, T const &old,
146+
std::chrono::time_point<Clock, Duration> const &timeout_time,
147+
std::memory_order order = std::memory_order_acquire ) {
148+
149+
return daw::atomic_wait_for( object, std::move( old ),
150+
timeout_time - Clock::now( ), order );
151+
}
152+
153+
/**
154+
* Waits until the atomic object satisfies the given predicate.
155+
*
156+
* This function blocks the calling thread until the value of the atomic
157+
* object pointed to by `object` satisfies the provided `predicate`.
158+
*
159+
* @tparam T The type of the atomic object.
160+
* @tparam Fn A templated function type that takes a value of type T and
161+
* returns bool.
162+
*
163+
* @param object Pointer to the atomic object to wait on.
164+
* @param predicate A callable object (such as a lambda or function object)
165+
* that takes a value of type T and returns a boolean indicating whether the
166+
* condition is met.
167+
* @param order The memory order to use. Defaults to
168+
* `std::memory_order_acquire`.
169+
*/
170+
template<typename T>
171+
void atomic_wait_if( std::atomic<T> const *object,
172+
Fn<bool( T )> auto &&predicate,
173+
std::memory_order order = std::memory_order_acquire ) {
174+
auto current =
175+
std::atomic_load_explicit( object, std::memory_order_acquire );
176+
auto const &const_current = current;
177+
while( not predicate( const_current ) ) {
178+
std::atomic_wait_explicit( object, current, order );
179+
current = std::atomic_load_explicit( object, std::memory_order_relaxed );
180+
}
181+
}
182+
183+
/**
184+
* Waits for an atomic object to satisfy a predicate within a specified time
185+
* duration.
186+
*
187+
* This function blocks the calling thread until the predicate applied
188+
* to the atomic object evaluates to true or the specified timeout period
189+
* elapses.
190+
*
191+
* @tparam T The type of the atomic object.
192+
* @tparam Fn A callable type that returns a boolean value when applied to an
193+
* object of type T.
194+
* @tparam Rep A type representing the number of ticks of the time duration.
195+
* @tparam Period A std::ratio type representing the tick period of the time
196+
* duration.
197+
*
198+
* @param object Pointer to the atomic object to wait on.
199+
* @param predicate Predicate function to apply to the atomic object's value.
200+
* @param rel_time The relative time duration to wait for the predicate to
201+
* become true.
202+
* @param order The memory order to use for atomic operations. Defaults to
203+
* `std::memory_order_acquire`.
204+
*
205+
* @return Returns wait_status::found if the predicate evaluates to true
206+
* before the timeout, and wait_status::timeout otherwise.
207+
*/
208+
template<typename T, typename Rep, typename Period>
209+
[[nodiscard]] wait_status
210+
atomic_wait_if_for( std::atomic<T> const *object,
211+
Fn<bool( T )> auto &&predicate,
212+
std::chrono::duration<Rep, Period> const &rel_time,
213+
std::memory_order order = std::memory_order_acquire ) {
214+
auto const final_time =
215+
std::chrono::high_resolution_clock::now( ) + rel_time;
216+
auto current_value =
217+
std::atomic_load_explicit( object, std::memory_order_acquire );
218+
219+
for( int tries = 0; not predicate( current_value ) and tries < 16;
220+
++tries ) {
221+
std::this_thread::yield( );
222+
current_value = std::atomic_load_explicit( object, order );
223+
}
224+
auto const poll_fn = [&] {
225+
current_value = std::atomic_load_explicit( object, order );
226+
auto const pred_result = predicate( current_value );
227+
if( pred_result ) {
228+
return true;
229+
}
230+
auto const current_time = std::chrono::high_resolution_clock::now( );
231+
auto const is_timed_out = current_time >= final_time;
232+
return is_timed_out;
233+
};
234+
(void)atomic_impl::poll_with_backoff( atomic_impl::timed_backoff_policy,
235+
poll_fn, rel_time );
236+
if( predicate( current_value ) ) {
237+
return wait_status::found;
238+
}
239+
return wait_status::timeout;
240+
}
241+
242+
/**
243+
* Waits until the atomic object satisfies the given predicate or the absolute
244+
* timeout time is reached.
245+
*
246+
* This function blocks the calling thread until the atomic value pointed to
247+
* by `object` satisfies the `predicate` function or the specified timeout
248+
* time `timeout_time` is reached.
249+
*
250+
* @tparam T The type of the atomic object.
251+
* @tparam Clock The clock used for the timeout.
252+
* @tparam Duration The duration type used by the clock.
253+
* @tparam Fn A callable type that takes a value of type T and returns a bool.
254+
*
255+
* @param object Pointer to the atomic object to wait on.
256+
* @param predicate A callable that takes the atomic object's value and
257+
* returns whether it satisfies the condition.
258+
* @param timeout_time The absolute time point by which the wait operation
259+
* should be aborted if the predicate is not satisfied.
260+
* @param order The memory order to use. Defaults to
261+
* std::memory_order_acquire.
262+
*
263+
* @return A `wait_status` enum indicating whether the predicate was satisfied
264+
* or a timeout occurred.
265+
*/
266+
template<typename T, typename Clock, typename Duration>
267+
[[nodiscard]] wait_status atomic_wait_if_until(
268+
std::atomic<T> const *object, Fn<bool( T )> auto &&predicate,
269+
std::chrono::time_point<Clock, Duration> const &timeout_time,
270+
std::memory_order order = std::memory_order_acquire ) {
271+
return daw::atomic_wait_if_for( object, std::move( predicate ),
272+
timeout_time - Clock::now( ), order );
273+
}
274+
275+
template<typename T>
276+
/**
277+
* Waits until the atomic object reaches the desired value.
278+
*
279+
* This function blocks the calling thread until the atomic value
280+
* pointed to by `object` equals `desired_value`.
281+
*
282+
* @tparam T The type of the atomic object.
283+
*
284+
* @param object Pointer to the atomic object to wait on.
285+
* @param desired_value The value that the atomic object is expected to reach.
286+
* @param order The memory order to use. Defaults to
287+
* `std::memory_order_acquire`.
288+
*/
289+
void atomic_wait_value_equal(
290+
std::atomic<T> const *object, T const &desired_value,
291+
std::memory_order order = std::memory_order_acquire ) {
292+
atomic_wait_if(
293+
object,
294+
[&desired_value]( T const &current_value ) {
295+
return current_value == desired_value;
296+
},
297+
order );
298+
}
299+
300+
/**
301+
* Waits until the atomic object reaches the desired value or the relative
302+
* time duration has elapsed.
303+
*
304+
* This function blocks the calling thread until the atomic value pointed to
305+
* by `object` equals `desired_value` or the specified relative time duration
306+
* `rel_time` has elapsed.
307+
*
308+
* @tparam T The type of the atomic object.
309+
* @tparam Rep The type representing the number of ticks.
310+
* @tparam Period The type representing the tick period.
311+
*
312+
* @param object Pointer to the atomic object to wait on.
313+
* @param desired_value The value that the atomic object is expected to reach.
314+
* @param rel_time The maximum duration to wait for the atomic object to reach
315+
* the desired value.
316+
* @param order The memory order to use. Defaults to
317+
* `std::memory_order_acquire`.
318+
*
319+
* @return A `wait_status` enum indicating whether the desired value was found
320+
* or a timeout occurred.
321+
*/
322+
template<typename T, typename Rep, typename Period>
323+
[[nodiscard]] wait_status atomic_wait_value_equal_for(
324+
std::atomic<T> const *object, T const &desired_value,
325+
std::chrono::duration<Rep, Period> const &rel_time,
326+
std::memory_order order = std::memory_order_acquire ) {
327+
return atomic_wait_if_for(
328+
object,
329+
[&desired_value]( T const &current_value ) {
330+
return current_value == desired_value;
331+
},
332+
rel_time, order );
333+
}
334+
335+
/**
336+
* Waits until the specified object reaches the desired value or the timeout
337+
* is reached.
338+
*
339+
* This function blocks the calling thread until the atomic value pointed to
340+
* by `object` equals `desired_value` or the specified timeout time
341+
* `timeout_time` is reached.
342+
*
343+
* @tparam T The type of the atomic object.
344+
* @tparam Clock The clock used for the timeout.
345+
* @tparam Duration The duration type used by the clock.
346+
*
347+
* @param object Pointer to the atomic object to wait on.
348+
* @param desired_value The value that the atomic object is expected to reach.
349+
* @param timeout_time The absolute time point by which the wait operation
350+
* should be aborted if the desired value is not reached.
351+
* @param order The memory order to use. Defaults to
352+
* `std::memory_order_acquire`.
353+
*
354+
* @return A `wait_status` enum indicating whether the desired value was found
355+
* or a timeout occurred.
356+
*/
357+
template<typename T, typename Clock, typename Duration>
358+
[[nodiscard]] wait_status atomic_wait_value_equal_until(
359+
std::atomic<T> const *object, T const &desired_value,
360+
std::chrono::time_point<Clock, Duration> const &timeout_time,
361+
std::memory_order order = std::memory_order_acquire ) {
362+
return daw::atomic_wait_if_for(
363+
object,
364+
[&desired_value]( T const &current_value ) {
365+
return current_value == desired_value;
366+
},
367+
timeout_time - Clock::now( ), order );
368+
}
369+
370+
} // namespace daw

include/daw/daw_attributes.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,3 +121,11 @@
121121
#else
122122
#define DAW_ATTRIB_RETNOTNULL
123123
#endif
124+
125+
#if defined( DAW_HAS_GCC_LIKE )
126+
#define DAW_ATTRIB_NONNULL(...) [[gnu::nonnull __VA_ARGS__]]
127+
#define DAW_ATTRIB_RET_NONNULL [[gnu::returns_nonnull]]
128+
#else
129+
#define DAW_ATTRIB_NONNULL(...)
130+
#define DAW_ATTRIB_RET_NONNULL
131+
#endif

0 commit comments

Comments
 (0)