C++20 std::atomic
has wait
and notify_*
member functions, but no wait_for
/wait_until
.
The Microsoft STL implementation for std::atomic
uses WaitOnAddress
(when the OS is new enough to has it). And this API has a dwMilliseconds
parameter just as timeout value. So from a standard library writer's standpoint, I think the missing functions are easily implementable (at least on Windows 8 or newer). I just wonder why it's not in C++20.
But as a (portable) user-code writer, I have to emulate the behavior with a standard semaphore and an atomic counter. So here's the code:
#include <concepts>
#include <atomic>
#include <type_traits>
#include <cstring>
#include <semaphore>
namespace detail
{
template <size_t N>
struct bytes
{
unsigned char space[N];
auto operator<=>(bytes const &) const = default;
};
//Compare by value representation, as requested by C++20.
//The implementation is a bit awkward.
//Hypothetically `std::atomic<T>::compare(T, T)` would be helpful. :)
template <std::integral T>
bool compare(T a, T b) noexcept
{
static_assert(std::has_unique_object_representations_v<T>);
return a == b;
}
template <typename T>
requires(std::has_unique_object_representations_v<T> && !std::integral<T>)
bool compare(T a, T b) noexcept
{
bytes<sizeof(T)> aa, bb;
std::memcpy(aa.space, &a, sizeof(T));
std::memcpy(bb.space, &b, sizeof(T));
return aa == bb;
}
template <typename T>
requires(!std::has_unique_object_representations_v<T>)
bool compare(T a, T b) noexcept
{
std::atomic<T> aa{ a };
auto equal = aa.compare_exchange_strong(b, b, std::memory_order_relaxed);
return equal;
}
template <typename T>
class atomic_with_timed_wait
: public std::atomic<T>
{
private:
using base_atomic = std::atomic<T>;
std::counting_semaphore<> mutable semaph{ 0 };
std::atomic<std::ptrdiff_t> mutable notify_demand{ 0 };
public:
using base_atomic::base_atomic;
public:
void notify_one() /*noexcept*/
{
auto nd = notify_demand.load(std::memory_order_relaxed);
if (nd <= 0)
return;
notify_demand.fetch_sub(1, std::memory_order_relaxed);
semaph.release(1);//may throw
}
void notify_all() /*noexcept*/
{
auto nd = notify_demand.exchange(0, std::memory_order_relaxed);
if (nd > 0)
{
semaph.release(nd);//may throw
}
else if (nd < 0)
{
//Overly released. Put it back.
notify_demand.fetch_add(nd, std::memory_order_relaxed);
}
}
void wait(T old, std::memory_order order = std::memory_order::seq_cst) const /*noexcept*/
{
for (;;)
{
T const observed = base_atomic::load(order);
if (false == compare(old, observed))
return;
notify_demand.fetch_add(1, std::memory_order_relaxed);
semaph.acquire();//may throw
//Acquired.
}
}
template <typename TPoint>
bool wait_until(int old, TPoint const & abs_time, std::memory_order order = std::memory_order::seq_cst) const /*noexcept*/
//Returns: true->diff; false->timeout
{
for (;;)
{
T const observed = base_atomic::load(order);
if (false == compare(old, observed))
return true;
notify_demand.fetch_add(1, std::memory_order_relaxed);
if (semaph.try_acquire_until(abs_time))//may throw
{
//Acquired.
continue;
}
else
{
//Not acquired and timeout.
//This might happen even if semaph has positive release counter.
//Just cancel demand and return.
//Note that this might give notify_demand a negative value,
// which means the semaph is overly released.
//Subsequent acquire on semaph would just succeed spuriously.
//So it should be OK.
notify_demand.fetch_sub(1, std::memory_order_relaxed);
return false;
}
}
}
//TODO: bool wait_for()...
};
}
using detail::atomic_with_timed_wait;
I am just not sure whether it's correct. So, is there any problem in this code?