我有一个程序,我想保证在循环内取得进展,因为它需要响应安全关键事件。我的想法是为可以在该循环内进行的所有函数调用设置超时,但我需要原子数据类型。 std::atomic
#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
我尝试寻找 std::atomic::try_lock_for,类似于 timed_mutex 并找到了这个 C++20: How to wait on anatomic object with timeout?,但等待函数是我想说的其他东西。
std::atomic
的预期用例是在您关心的实现上无锁的类型。
某些系统上较大类型的回退到锁定只是为了实现可移植性。
对于无锁原子,没有尝试,只是做。 (这里插入“不要”笑话,我没想到一个好笑话。)
除非像
.fetch_or
这样的东西必须在x86上通过CAS重试循环来实现,如果您以不允许lock bts
(位测试和设置)的方式使用返回值,则与.fetch_add
不同有特别说明lock xadd
。
最常见的情况确实有一个“尝试”,但是:
compare_exchange_weak
只是一次LL/SC尝试,即使值实际上匹配,也可能由于缓存行的争用而出现“虚假”失败。 需要重试循环来在仅具有加载链接/存储条件原子的机器上实现 compare_exchange_strong
,就像过去几年前的典型 RISC,其中 可扩展性到大量核心成为一个大问题。 (ARMv8.1 具有单指令原子 RMW。)
纯加载和纯存储永远不需要重试,除非使用 LL/SC 实现,类型太宽而无法使用纯加载/存储实现原子化。 例如 32 位 ARM 上的
int64_t
(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
所以这里有一个重试循环。 但 ISO C++ 不会公开 LL/SC 或 CAS 重试,除了
compare_exchange_weak
。
它仍然是无锁的,至少如果硬件进行一些仲裁以防止 LL/SC 系统上的活锁(如果许多内核同时尝试),例如让一个内核在足够的周期内保留缓存行的所有权以完成 SC 部分只要交易不被中断即可。