/* * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. * * Copyright 2019 Danny Robson */ #pragma once #include "../thread/primitive.hpp" #include "../thread/spinlock.hpp" #include "../view.hpp" #include #include #include #include namespace cruft::parallel { /// A trivial thread safe stack with a fixed capacity. /// /// This implementation uses a spinlock, so don't try to push large /// objects under high contention. In the future it may be replaced with a /// different locking mechanism. template class stack { public: stack (stack const &) = delete; stack& operator=(stack const &) = delete; stack (stack &&rhs): stack (0) { swap (rhs); } stack& operator= (stack &&rhs) { swap (rhs); return *this; } stack clone (void) const { stack res (capacity ()); std::lock_guard lk (m_lock); res.m_cursor = m_cursor.load (); res.m_store = m_store; return res; } /// Thread safe swapping with the supplied object void swap (stack &rhs) { std::lock_guard lk0 (m_lock); std::lock_guard lk1 (rhs.m_lock); auto tmp = m_cursor.load (); m_cursor = rhs.m_cursor.load (); rhs.m_cursor = tmp; std::swap (m_store, rhs.m_store); std::swap (m_lock, rhs.m_lock); } stack (std::size_t capacity) : m_cursor (0) , m_store (capacity) { // NOTE: a capacity of zero must be supported as it forms part of // how we structure the move operations } /// Move the value from the top of the stack into an output pointer. /// /// The internal object will be destroyed after the output has been set. /// /// The value `true` will be returned if the value was successfully /// copied. If no object is available for popping, or locking failed, /// the value `false` will be returned. /// /// NOTE: There are no exception guarantees at this time. bool pop [[nodiscard]] (ValueT *out) { std::lock_guard lk (m_lock); if (m_cursor == 0) return false; auto ptr = reinterpret_cast (&m_store[--m_cursor]); *out = std::move (*ptr); ptr->~ValueT (); return true; } /// Constructs a ValueT on the top of the stack. /// /// The value `true` will be returned if the object was successfully /// stored. In the event of a lack of capacity, locking failure, or /// other error the value `false` will be returned and the user may /// retry the operation. /// /// NOTE: There are no exception guarantees at this time. template bool push [[nodiscard]] (InputT &&arg) { std::lock_guard lk (m_lock); if (m_cursor >= m_store.size ()) return false; auto ptr = reinterpret_cast (&m_store[m_cursor++]); new (ptr) ValueT (std::forward (arg)); return true; } std::size_t capacity (void) const { return m_store.size (); } std::size_t size (void) const { return m_cursor; } bool empty (void) const { return m_cursor == 0; } void clear (void) { std::lock_guard lk (m_lock); for (auto &raw: store (contract::I_HAVE_LOCKED_THIS_STRUCTURE)) raw.~ValueT (); m_cursor = 0; } // DO NOT make this enum easier to use. It's supposed to be annoying // so that people don't use it. enum class contract { I_HAVE_LOCKED_THIS_STRUCTURE }; /// Returns the currently available data. /// /// Data is laid out bottom-to-top of the stack. /// /// DO NOT call this if there are any other potential clients /// accessing the structure as it will not be protected by locks. cruft::view store (contract) { auto begin = reinterpret_cast (m_store.data ()); auto end = begin + m_cursor.load (); return cruft::view { begin, end }; } private: using index_type = std::size_t; struct raw_type { alignas(ValueT) std::byte data[sizeof(ValueT)]; }; mutable cruft::thread::spinlock m_lock; std::atomic m_cursor; std::vector m_store; }; }