ref:main
/**
* @file src/thread_safe.h
* @brief Declarations for thread-safe data structures.
*/
#pragma once
// standard includes
#include <array>
#include <atomic>
#include <condition_variable>
#include <functional>
#include <map>
#include <mutex>
#include <vector>
// local includes
#include "utility.h"
namespace safe {
/**
* @brief Thread-safe event value that blocks readers until a value is raised.
*/
template<class T>
class event_t {
public:
/**
* @brief Status value stored in the event.
*/
using status_t = util::optional_t<T>;
/**
* @brief Notify waiters that a new event value is available.
*
* @param args Arguments forwarded to the callable or parser.
*/
template<class... Args>
void raise(Args &&...args) {
std::lock_guard lg {_lock};
if (!_continue) {
return;
}
if constexpr (std::is_same_v<std::optional<T>, status_t>) {
_status = std::make_optional<T>(std::forward<Args>(args)...);
} else {
_status = status_t {std::forward<Args>(args)...};
}
_cv.notify_all();
}
/**
* @brief Try to remove and return the next queued item without any further wait
*
* @return Removed queue item, or empty result when the queue is stopped or empty.
*/
status_t try_pop() {
std::lock_guard lg {_lock};
if (!_status) {
return util::false_v<status_t>;
}
auto val = std::move(_status);
_status = util::false_v<status_t>;
return val;
}
// pop and view should not be used interchangeably
/**
* @brief Remove and return the next queued item, waiting when requested.
*
* @return Removed queue item, or empty result when the queue is stopped or empty.
*/
status_t pop() {
std::unique_lock ul {_lock};
if (!_continue) {
return util::false_v<status_t>;
}
while (!_status) {
_cv.wait(ul);
if (!_continue) {
return util::false_v<status_t>;
}
}
auto val = std::move(_status);
_status = util::false_v<status_t>;
return val;
}
// pop and view should not be used interchangeably
/**
* @brief Remove and return the next queued item, waiting when requested.
*
* @param delay Maximum wait duration before timing out.
* @return Removed queue item, or empty result when the queue is stopped or empty.
*/
template<typename Rep, typename Period>
status_t pop(std::chrono::duration<Rep, Period> delay) {
std::unique_lock ul {_lock};
if (bool success = _cv.wait_for(ul, delay, [this] {
return (bool) _status || !_continue;
});
!success || !_continue) {
return util::false_v<status_t>;
}
auto val = std::move(_status);
_status = util::false_v<status_t>;
return val;
}
// pop and view should not be used interchangeably
/**
* @brief Read the current value without removing it from the queue.
*
* @return Optional copy of the front item, or empty status when the queue is closed.
*/
status_t view() {
std::unique_lock ul {_lock};
if (!_continue) {
return util::false_v<status_t>;
}
while (!_status) {
_cv.wait(ul);
if (!_continue) {
return util::false_v<status_t>;
}
}
return _status;
}
// pop and view should not be used interchangeably
/**
* @brief Read the current value without removing it from the queue.
*
* @param delay Maximum wait duration before timing out.
* @return Optional copy of the front item, or empty status after timeout/closure.
*/
template<class Rep, class Period>
status_t view(std::chrono::duration<Rep, Period> delay) {
std::unique_lock ul {_lock};
if (!_continue) {
return util::false_v<status_t>;
}
while (!_status) {
if (!_continue || _cv.wait_for(ul, delay) == std::cv_status::timeout) {
return util::false_v<status_t>;
}
}
return _status;
}
/**
* @brief Inspect the next queued value without popping it.
*
* @return True when a value is available to inspect.
*/
bool peek() {
return _continue && (bool) _status;
}
/**
* @brief Raise the alarm and wake all waiting threads.
*/
void stop() {
std::lock_guard lg {_lock};
_continue = false;
_cv.notify_all();
}
/**
* @brief Reset the object to its initial empty state.
*/
void reset() {
std::lock_guard lg {_lock};
_continue = true;
_status = util::false_v<status_t>;
}
/**
* @brief Return whether the queue is still running.
*
* @return True while the queue accepts producers and consumers.
*/
[[nodiscard]] bool running() const {
return _continue;
}
private:
bool _continue {true};
status_t _status {util::false_v<status_t>};
std::condition_variable _cv;
std::mutex _lock;
};
/**
* @brief Alarm primitive used to wait for and raise shutdown-style signals.
*/
template<class T>
class alarm_raw_t {
public:
/**
* @brief Status value carried by the alarm.
*/
using status_t = util::optional_t<T>;
/**
* @brief Mark the alarm as rung and wake waiting threads.
*
* @param status Native status code returned by the platform API.
*/
void ring(const status_t &status) {
std::lock_guard lg(_lock);
_status = status;
_rang = true;
_cv.notify_one();
}
/**
* @brief Mark the alarm as rung and wake waiting threads.
*
* @param status Native status code returned by the platform API.
*/
void ring(status_t &&status) {
std::lock_guard lg(_lock);
_status = std::move(status);
_rang = true;
_cv.notify_one();
}
/**
* @brief Wait for for.
*
* @param rel_time Rel time.
* @return Wait result indicating whether the condition was satisfied before timeout.
*/
template<class Rep, class Period>
auto wait_for(const std::chrono::duration<Rep, Period> &rel_time) {
std::unique_lock ul(_lock);
return _cv.wait_for(ul, rel_time, [this]() {
return _rang;
});
}
/**
* @brief Wait for for.
*
* @param rel_time Rel time.
* @param pred Predicate used to decide whether the wait is complete.
* @return Wait result indicating whether the condition was satisfied before timeout.
*/
template<class Rep, class Period, class Pred>
auto wait_for(const std::chrono::duration<Rep, Period> &rel_time, Pred &&pred) {
std::unique_lock ul(_lock);
return _cv.wait_for(ul, rel_time, [this, &pred]() {
return _rang || pred();
});
}
/**
* @brief Wait for until.
*
* @param rel_time Rel time.
* @return Wait result indicating whether the condition was satisfied before the deadline.
*/
template<class Rep, class Period>
auto wait_until(const std::chrono::duration<Rep, Period> &rel_time) {
std::unique_lock ul(_lock);
return _cv.wait_until(ul, rel_time, [this]() {
return _rang;
});
}
/**
* @brief Wait for until.
*
* @param rel_time Rel time.
* @param pred Predicate used to decide whether the wait is complete.
* @return Wait result indicating whether the condition was satisfied before the deadline.
*/
template<class Rep, class Period, class Pred>
auto wait_until(const std::chrono::duration<Rep, Period> &rel_time, Pred &&pred) {
std::unique_lock ul(_lock);
return _cv.wait_until(ul, rel_time, [this, &pred]() {
return _rang || pred();
});
}
/**
* @brief Block until the event is raised.
*
* @return Lock object held after the event is observed.
*/
auto wait() {
std::unique_lock ul(_lock);
_cv.wait(ul, [this]() {
return _rang;
});
}
/**
* @brief Block until the event is raised and the predicate accepts the state.
*
* @param pred Predicate used to decide whether the wait is complete.
* @return Lock object held after the predicate succeeds.
*/
template<class Pred>
auto wait(Pred &&pred) {
std::unique_lock ul(_lock);
_cv.wait(ul, [this, &pred]() {
return _rang || pred();
});
}
/**
* @brief Return or update the current status value.
*
* @return Status status.
*/
const status_t &status() const {
return _status;
}
/**
* @brief Return or update the current status value.
*
* @return Status status.
*/
status_t &status() {
return _status;
}
/**
* @brief Reset the object to its initial empty state.
*/
void reset() {
_status = status_t {};
_rang = false;
}
private:
std::mutex _lock;
std::condition_variable _cv;
status_t _status {util::false_v<status_t>};
bool _rang {false};
};
/**
* @brief Shared alarm primitive used for cross-thread shutdown signals.
*/
template<class T>
using alarm_t = std::shared_ptr<alarm_raw_t<T>>;
/**
* @brief Create a alarm object or message.
*
* @return Constructed alarm object.
*/
template<class T>
alarm_t<T> make_alarm() {
return std::make_shared<alarm_raw_t<T>>();
}
/**
* @brief Thread-safe queue with blocking and shutdown-aware consumers.
*/
template<class T>
class queue_t {
public:
/**
* @brief Value type stored in the queue.
*/
using status_t = util::optional_t<T>;
/**
* @brief Construct a bounded blocking queue.
*
* @param max_elements Maximum number of queued elements before producers block.
*/
queue_t(std::uint32_t max_elements = 32):
_max_elements {max_elements} {
}
/**
* @brief Notify waiters that a new event value is available.
*
* @param args Arguments forwarded to the callable or parser.
*/
template<class... Args>
void raise(Args &&...args) {
std::lock_guard ul {_lock};
if (!_continue) {
return;
}
if (_queue.size() == _max_elements) {
_queue.clear();
}
_queue.emplace_back(std::forward<Args>(args)...);
_cv.notify_all();
}
/**
* @brief Inspect the next queued value without popping it.
*
* @return True when a value is available to inspect.
*/
bool peek() {
return _continue && !_queue.empty();
}
/**
* @brief Remove and return the next queued item, waiting when requested.
*
* @param delay Maximum wait duration before timing out.
* @return Removed queue item, or empty result when the queue is stopped or empty.
*/
template<class Rep, class Period>
status_t pop(std::chrono::duration<Rep, Period> delay) {
std::unique_lock ul {_lock};
if (!_continue) {
return util::false_v<status_t>;
}
while (_queue.empty()) {
if (!_continue || _cv.wait_for(ul, delay) == std::cv_status::timeout) {
return util::false_v<status_t>;
}
}
auto val = std::move(_queue.front());
_queue.erase(std::begin(_queue));
return val;
}
/**
* @brief Remove and return the next queued item, waiting when requested.
*
* @return Removed queue item, or empty result when the queue is stopped or empty.
*/
status_t pop() {
std::unique_lock ul {_lock};
if (!_continue) {
return util::false_v<status_t>;
}
while (_queue.empty()) {
_cv.wait(ul);
if (!_continue) {
return util::false_v<status_t>;
}
}
auto val = std::move(_queue.front());
_queue.erase(std::begin(_queue));
return val;
}
/**
* @brief Return the wrapped pointer without synchronization checks.
*
* @return Mutable queue storage for callers that already hold external synchronization.
*/
std::vector<T> &unsafe() {
return _queue;
}
/**
* @brief Shut down the queue and wake blocked consumers.
*/
void stop() {
std::lock_guard lg {_lock};
_continue = false;
_cv.notify_all();
}
/**
* @brief Return whether the queue is still running.
*
* @return True while the queue accepts producers and consumers.
*/
[[nodiscard]] bool running() const {
return _continue;
}
private:
bool _continue {true};
std::uint32_t _max_elements;
std::mutex _lock;
std::condition_variable _cv;
std::vector<T> _queue;
};
/**
* @brief Shared object storage with custom construction and destruction hooks.
*/
template<class T>
class shared_t {
public:
/**
* @brief Object type stored in shared protected storage.
*/
using element_type = T;
/**
* @brief Callback signature used to construct the protected object.
*/
using construct_f = std::function<int(element_type &)>;
/**
* @brief Callback signature used to destroy the protected object.
*/
using destruct_f = std::function<void(element_type &)>;
/**
* @brief Pointer wrapper state protected by synchronization.
*/
struct ptr_t {
shared_t *owner; ///< Shared object that owns the protected value.
ptr_t():
owner {nullptr} {
}
/**
* @brief Construct a reference bound to a shared protected object.
*
* @param owner Shared object that owns this pointer wrapper.
*/
explicit ptr_t(shared_t *owner):
owner {owner} {
}
/**
* @brief Move a shared-object reference without locking the protected object.
*
* @param ptr Pointer managed by the safe pointer wrapper.
*/
ptr_t(ptr_t &&ptr) noexcept:
owner {ptr.owner} {
ptr.owner = nullptr;
}
/**
* @brief Copy a shared-object reference without locking the protected object.
*
* @param ptr Pointer managed by the safe pointer wrapper.
*/
ptr_t(const ptr_t &ptr) noexcept:
owner {ptr.owner} {
if (!owner) {
return;
}
auto tmp = ptr.owner->ref();
tmp.owner = nullptr;
}
/**
* @brief Assign state from another instance while preserving ownership semantics.
*
* @param ptr Pointer managed by the safe pointer wrapper.
* @return Reference or value produced by the operator.
*/
ptr_t &operator=(const ptr_t &ptr) noexcept {
if (!ptr.owner) {
release();
return *this;
}
return *this = std::move(*ptr.owner->ref());
}
/**
* @brief Assign state from another instance while preserving ownership semantics.
*
* @param ptr Pointer managed by the safe pointer wrapper.
* @return Reference or value produced by the operator.
*/
ptr_t &operator=(ptr_t &&ptr) noexcept {
if (owner) {
release();
}
std::swap(owner, ptr.owner);
return *this;
}
~ptr_t() {
if (owner) {
release();
}
}
/**
* @brief Check whether this shared pointer wrapper owns a live object.
*/
operator bool() const {
return owner != nullptr;
}
/**
* @brief Release the COM or platform reference owned by the pointer.
*/
void release() {
std::lock_guard lg {owner->_lock};
if (!--owner->_count) {
owner->_destruct(*get());
(*this)->~element_type();
}
owner = nullptr;
}
/**
* @brief Return the currently wrapped value or handle.
*
* @return Underlying native handle or object pointer.
*/
element_type *get() const {
return reinterpret_cast<element_type *>(owner->_object_buf.data());
}
/**
* @brief Access the shared protected object.
*
* @return Pointer to the object stored in the shared wrapper.
*/
element_type *operator->() {
return reinterpret_cast<element_type *>(owner->_object_buf.data());
}
};
/**
* @brief Construct a shared protected object with custom close and destruction hooks.
*
* @param fc Callable invoked when the shared object is closed.
* @param fd Callable invoked when the shared object is destroyed.
*/
template<class FC, class FD>
shared_t(FC &&fc, FD &&fd):
_construct {std::forward<FC>(fc)},
_destruct {std::forward<FD>(fd)} {
}
/**
* @brief Return a shared reference to the protected object.
*
* @return Reference handle that keeps the protected object alive.
*/
[[nodiscard]] ptr_t ref() {
std::lock_guard lg {_lock};
if (!_count) {
new (_object_buf.data()) element_type;
if (_construct(*reinterpret_cast<element_type *>(_object_buf.data())) != 0) {
return ptr_t {nullptr};
}
}
++_count;
return ptr_t {this};
}
private:
construct_f _construct;
destruct_f _destruct;
std::array<std::uint8_t, sizeof(element_type)> _object_buf;
std::uint32_t _count;
std::mutex _lock;
};
/**
* @brief Create a shared object or message.
*
* @param fc Callable executed while the shared object is locked.
* @param fd Native file descriptor to wrap or inspect.
* @return Constructed shared object.
*/
template<class T, class F_Construct, class F_Destruct>
auto make_shared(F_Construct &&fc, F_Destruct &&fd) {
return shared_t<T> {
std::forward<F_Construct>(fc),
std::forward<F_Destruct>(fd)
};
}
/**
* @brief Boolean alarm used as a simple signal.
*/
using signal_t = event_t<bool>;
class mail_raw_t;
/**
* @brief Shared mailbox handle used by event and queue wrappers.
*/
using mail_t = std::shared_ptr<mail_raw_t>;
/**
* @brief Run cleanup for completed asynchronous work.
*
* @param mail Mailbox used to exchange messages with worker threads.
*/
void cleanup(mail_raw_t *);
/**
* @brief Wrapper that posts cleanup work to a mailbox when destroyed.
*/
template<class T>
class post_t: public T {
public:
/**
* @brief Construct a message-posting wrapper around an existing type.
*
* @param mail Mailbox used to exchange messages with worker threads.
* @param args Arguments forwarded to the wrapped type constructor.
*/
template<class... Args>
post_t(mail_t mail, Args &&...args):
T(std::forward<Args>(args)...),
mail {std::move(mail)} {
}
mail_t mail; ///< Mailbox kept alive until the posted object is destroyed.
~post_t() {
cleanup(mail.get());
}
};
/**
* @brief Acquire the underlying lock or keyed mutex.
*
* @param wp Weak pointer used to test whether the object is still alive.
* @return Lock guard owning the synchronized object until destruction.
*/
template<class T>
inline auto lock(const std::weak_ptr<void> &wp) {
return std::reinterpret_pointer_cast<typename T::element_type>(wp.lock());
}
/**
* @brief Mailbox backing store for events, queues, and posted cleanup objects.
*/
class mail_raw_t: public std::enable_shared_from_this<mail_raw_t> {
public:
/**
* @brief Mailbox-backed event wrapper for a typed value.
*/
template<class T>
using event_t = std::shared_ptr<post_t<event_t<T>>>;
/**
* @brief Mailbox-backed queue wrapper for typed messages.
*/
template<class T>
using queue_t = std::shared_ptr<post_t<queue_t<T>>>;
/**
* @brief Create a typed event channel from the raw mailbox.
*
* @param id Identifier for the controller, session, display, or resource.
* @return Typed event channel associated with the supplied identifier.
*/
template<class T>
event_t<T> event(const std::string_view &id) {
std::lock_guard lg {mutex};
auto it = id_to_post.find(id);
if (it != std::end(id_to_post)) {
return lock<event_t<T>>(it->second);
}
auto post = std::make_shared<typename event_t<T>::element_type>(shared_from_this());
id_to_post.emplace(std::pair<std::string, std::weak_ptr<void>> {std::string {id}, post});
return post;
}
/**
* @brief Create a typed queue channel from the raw mailbox.
*
* @param id Identifier for the controller, session, display, or resource.
* @return Typed queue channel associated with the supplied identifier.
*/
template<class T>
queue_t<T> queue(const std::string_view &id) {
std::lock_guard lg {mutex};
auto it = id_to_post.find(id);
if (it != std::end(id_to_post)) {
return lock<queue_t<T>>(it->second);
}
auto post = std::make_shared<typename queue_t<T>::element_type>(shared_from_this(), 32);
id_to_post.emplace(std::pair<std::string, std::weak_ptr<void>> {std::string {id}, post});
return post;
}
/**
* @brief Run cleanup for completed asynchronous work.
*/
void cleanup() {
std::lock_guard lg {mutex};
for (auto it = std::begin(id_to_post); it != std::end(id_to_post); ++it) {
auto &weak = it->second;
if (weak.expired()) {
id_to_post.erase(it);
return;
}
}
}
std::mutex mutex; ///< Mutex protecting the map of live posted objects.
std::map<std::string, std::weak_ptr<void>, std::less<>> id_to_post; ///< Posted objects keyed by cleanup identifier.
};
/**
* @brief Run cleanup for completed asynchronous work.
*/
inline void cleanup(mail_raw_t *mail) {
mail->cleanup();
}
} // namespace safe