Skip to content

Commit

Permalink
Fix build and follow general code style more closely
Browse files Browse the repository at this point in the history
  • Loading branch information
raftario committed Jun 7, 2021
1 parent d6e9020 commit 3f56f7a
Show file tree
Hide file tree
Showing 2 changed files with 46 additions and 70 deletions.
26 changes: 18 additions & 8 deletions shared/utils/lazy.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,18 @@ class Once {

/// @brief Gets the current value, if it is initialized.
std::optional<T const&> get() const {
return this->value;
return value;
}
//// @brief Gets the current value, if it is initialized.
std::optional<T&> get() {
return this->value;
return value;
}

/// @brief Gets the current value, or initializes it.
/// @param initializer Function to use for initialization if required.
T& get_or_initialize(std::function<T()> const& initializer) {
this->initialize(initializer);
return *this->value;
initialize(initializer);
return *value;
}
};

Expand All @@ -48,17 +48,27 @@ class Lazy {
/// @param initializer_ Function to use for initialization on the first access.
Lazy(F const& initializer_) : inner(), initializer(initializer_) {}

operator T() const {
return inner.get_or_initialize(initializer);
}
operator T const &() const {
return inner.get_or_initialize(initializer);
}
operator T&() {
return inner.get_or_initialize(initializer);
}

T const* operator->() const {
return this->inner.get_or_initialize(initializer);
return inner.get_or_initialize(initializer);
}
T* operator->() {
return this->inner.get_or_initialize(initializer);
return inner.get_or_initialize(initializer);
}

T const& operator*() const {
return this->inner.get_or_initialize(initializer);
return inner.get_or_initialize(initializer);
}
T& operator*() {
return this->inner.get_or_initialize(initializer);
return inner.get_or_initialize(initializer);
}
};
90 changes: 28 additions & 62 deletions src/utils/lazy.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,52 +19,42 @@ class Waiter {
Waiter* next;

Waiter(const std::uintptr_t current_state)
: lock(), cv(), signaled(false),
next(reinterpret_cast<Waiter*>(current_state & ~STATE_MASK)) {
// We need the alignment to be at least 4 to fit our state in the two
// lower bits of Waiter pointers. This should be the case 99% of the
// time.
: lock(), cv(), signaled(false), next(reinterpret_cast<Waiter*>(current_state & ~STATE_MASK)) {
// We need the alignment to be at least 4 to fit our state in the two lower bits of Waiter pointers. This should be the case 99% of the time.
static_assert(alignof(Waiter) >= 4);
}

void wait() {
std::unique_lock<std::mutex> lock(this->lock);
while (!this->signaled.load(std::memory_order_acquire)) {
while (!signaled.load(std::memory_order_acquire)) {
cv.wait(lock);
}
}

void notify() {
this->signaled.store(true, std::memory_order_release);
this->cv.notify_one();
signaled.store(true, std::memory_order_release);
cv.notify_one();
}
};

class WaiterQueue {
// This is a reference to the state_and_queue of Once<T>. Since the
// WaiterQueue is a short lived value which only ever exists on the stack,
// having a reference member is fine.
// This is a reference to the state_and_queue of Once<T>. Since the WaiterQueue is a short lived value which only ever exists on the stack, having a reference member is fine.
std::atomic_uintptr_t& state_and_queue;

public:
// Value the state will be reset to when the destructor is run. This doesn't
// need to be atomic as it is only ever accessed by the initializing thread.
// Value the state will be reset to when the destructor is run. This doesn't need to be atomic as it is only ever accessed by the initializing thread.
std::uintptr_t set_state_on_destroy_to;

constexpr WaiterQueue(std::atomic_uintptr_t& state_and_queue_,
std::uintptr_t set_state_on_destroy_to_)
: state_and_queue(state_and_queue_),
set_state_on_destroy_to(set_state_on_destroy_to_) {}
constexpr WaiterQueue(std::atomic_uintptr_t& state_and_queue_, std::uintptr_t set_state_on_destroy_to_)
: state_and_queue(state_and_queue_), set_state_on_destroy_to(set_state_on_destroy_to_) {}

~WaiterQueue() {
// Grab the queue and swap it with the value it should have when
// destroyed.
auto const state_and_queue = this->state_and_queue.exchange(
this->set_state_on_destroy_to, std::memory_order_acq_rel);
// Grab the queue and swap it with the value it should have when destroyed.
auto const state_and_queue = this->state_and_queue.exchange(set_state_on_destroy_to, std::memory_order_acq_rel);

assert((state_and_queue & STATE_MASK) == RUNNING);

// Iterate over waiters in the queue and wake them up
// Iterate over waiters in the queue and wake them up.
auto next = reinterpret_cast<Waiter*>(state_and_queue & ~STATE_MASK);
while (next != nullptr) {
auto tmp = next->next;
Expand All @@ -79,59 +69,35 @@ void Once<T>::initialize(std::function<T()> const& initializer) {
auto state_and_queue =
this->state_and_queue.load(std::memory_order_acquire);
while (true) {
if (state_and_queue == INIT) [[likely]] {
if (state_and_queue == INIT) {
return;
} else if (state_and_queue == UNINIT) /* initialisation */ {
// Set the current state to RUNNING, but only if another thread
// hasn't already since the previous load. If that's the case,
// we keep looping (the value of state_and_queue will be
// updated).
// Set the current state to RUNNING, but only if another thread hasn't already since the previous load. If that's the case, we keep looping (the value of state_and_queue will be updated).
auto const exchanged =
this->state_and_queue.compare_exchange_strong(
state_and_queue, RUNNING,
std::memory_order_acquire);
this->state_and_queue.compare_exchange_strong(state_and_queue, RUNNING, std::memory_order_acquire);
if (!exchanged) {
continue;
}

// Create the waiter queue, telling it to set the state to
// UNINIT when destructed.
WaiterQueue waiter_queue(this->state_and_queue,
UNINIT);
// Initialize the value. If the initializer throws, the waiter
// queue destructor runs and the state is reset to UNINIT.
this->value = initializer();

// If this is reached the initializer has run successfully and
// the value is initalized, so we can tell the waiter queue to
// set the state to INIT when destructed (which happens
// immediately cause we return).
// Create the waiter queue, telling it to set the state to UNINIT when destructed.
WaiterQueue waiter_queue(this->state_and_queue, UNINIT);
// Initialize the value. If the initializer throws, the waiter queue destructor runs and the state is reset to UNINIT.
value = initializer();

// If this is reached the initializer has run successfully and the value is initalized, so we can tell the waiter queue to set the state to INIT when destructed (which happens immediately cause we return).
waiter_queue.set_state_on_destroy_to = INIT;
return;
} else [[unlikely]] /* waiting */ {
assert((state_and_queue & STATE_MASK) ==
RUNNING);

while ((state_and_queue & STATE_MASK) ==
RUNNING) {
// Construct a waiter on the current thread's stack, with
// its next pointer pointing to the current head of the
// waiter queue.
} else /* waiting */ {
assert((state_and_queue & STATE_MASK) == RUNNING);

while ((state_and_queue & STATE_MASK) == RUNNING) {
// Construct a waiter on the current thread's stack, with its next pointer pointing to the current head of the waiter queue.
Waiter node(state_and_queue);
auto const me = reinterpret_cast<uintptr_t>(&node);

// Add our waiter to the queue, but only if the current head
// has not been changed by another thread (if we don't check
// we might leak a waiter and deadlock its thread). It's
// fine to reference our stack local waiter from the queue
// because we will be blocking the thread (and therefore
// keeping our stack alive) until the queue itself is
// destroyed.
// Add our waiter to the queue, but only if the current head has not been changed by another thread (if we don't check we might leak a waiter and deadlock its thread). It's fine to reference our stack local waiter from the queue because we will be blocking the thread (and therefore keeping our stack alive) until the queue itself is destroyed.
auto const exchanged =
this->state_and_queue.compare_exchange_strong(
state_and_queue, me | RUNNING,
std::memory_order_release,
std::memory_order_relaxed);
this->state_and_queue.compare_exchange_strong(state_and_queue, me | RUNNING, std::memory_order_release, std::memory_order_relaxed);
if (!exchanged) {
continue;
}
Expand Down

0 comments on commit 3f56f7a

Please sign in to comment.