Reputation: 9
I have a program where I want to guarantee that progress is made inside a loop because it needs to respond to safety critical events. My idea was to have a timeout for all function calls that can be made inside this loop, but I needed atomic data types. std::atomic<my_struct> would have been sufficient here, but there are no try_lock_for functions right? So why does something like the following not exist in stdlib C++ yet?
#ifndef TIMED_ATOMIC_HPP
#define TIMED_ATOMIC_HPP
#include <mutex>
#include <chrono>
#include <expected>
#include <memory>
#include <type_traits>
template<typename T>
struct is_shared_ptr : std::false_type {};
template <typename U>
struct is_shared_ptr<std::shared_ptr<U>> : std::true_type {};
template <typename T>
struct is_unique_ptr : std::false_type {};
template <typename U, typename D>
struct is_unique_ptr<std::unique_ptr<U, D>> : std::true_type {};
template <typename T>
struct is_raw_pointer : std::is_pointer<T> {};
template <typename T>
concept NotPointerType = !is_raw_pointer<T>::value && !is_unique_ptr<T>::value && !is_shared_ptr<T>::value;
template<typename T>
concept AllowedType = NotPointerType<T> && std::is_copy_constructible_v<T> && std::is_move_constructible_v<T>;
/// @brief Wrapper class to allow threadsafe access with a timeout. This is required because the robot state update thread should never hang because it is responsible for stopping the robot in case exit_flag is set.
template<typename T>
requires AllowedType<T>
class timed_atomic {
private:
std::timed_mutex mtx {};
T value {};
public:
explicit timed_atomic(T initial_value) : value{initial_value} {}
timed_atomic() = default;
std::expected<T, std::monostate> try_load_for(std::chrono::microseconds timeout_us) {
std::unique_lock lock{mtx, std::defer_lock};
if(!lock.try_lock_for(timeout_us)) {
return std::unexpected{std::monostate{}};
}
return value;
}
bool try_store_for(T new_value, std::chrono::microseconds timeout_us) {
std::unique_lock lock{mtx, std::defer_lock};
if(!lock.try_lock_for(timeout_us)) {
return false;
}
value = std::move(new_value);
return true;
}
};
#endif
I tried looking for std::atomic::try_lock_for, similar to timed_mutex and found this C++20: How to wait on an atomic object with timeout?, but the wait function is something else I would say.
Update: Here is some more context: "Real-time" multithreaded design with timeouts
Upvotes: 0
Views: 73
Reputation: 365517
The intended use-case for std::atomic
is types that are lock-free on the implementations you care about. std::atomic<T>::is_always_lock_free()
or a.is_lock_free()
.
https://en.cppreference.com/w/cpp/atomic/atomic/is_always_lock_free
The fallback to locking for larger types on some system is just to enable portability.
For lock-free atomics, there is no try, just do. (Insert "do not" joke here, I didn't think of a good one.)
Except when something like .fetch_or
has to get implemented with a CAS retry loop on x86 if you use the return value in a way that doesn't allow lock bts
(bit test-and-set), unlike .fetch_add
which has a special instruction lock xadd
.
The most common case does have a "try", though: compare_exchange_weak
is just one LL/SC attempt which can have "spurious" failure due to contention for the cache line, even if the value actually matches. A retry loop is needed to implement compare_exchange_strong
on machines that only have load-linked/store-conditional atomics, like typical RISCs before the last few years where scalability to huge core counts became a big deal. (ARMv8.1 has single-instruction atomic RMWs.)
Pure loads and pure stores never need to retry, unless implemented with LL/SC for types too wide to be atomic with plain loads/stores. Such as int64_t
on 32-bit ARM (Godbolt)
void store64(std::atomic<int64_t> &a, int64_t val){
a.store(val, std::memory_order_relaxed);
}
store64(std::atomic<long long>&, long long):
push {r4, r5}
.L6:
ldrexd r4, r5, [r0]
strexd r1, r2, r3, [r0]
cmp r1, #0
bne .L6
pop {r4, r5}
bx lr
So there is a retry-loop here. But ISO C++ doesn't expose LL/SC or CAS retries other than compare_exchange_weak
.
It's still lock-free, at least if hardware does some arbitration to prevent live-lock on LL/SC systems if many cores are trying simultaneously, like letting a core hang onto ownership of a cache line for enough cycles to complete the SC part of a transaction as long as it isn't interrupted.
Upvotes: 1