/** * @file CircularQueue.h * @author apio (cloudapio.eu) * @brief Lock-free FIFO data structures. * * @copyright Copyright (c) 2022-2023, the Luna authors. * */ #pragma once #include #include #include #include /** * @brief An atomic lock-free circular FIFO queue. * * @tparam T The type of data to store in this queue. * @tparam Size The amount of elements to make space for. */ template class CircularQueue { enum { Capacity = Size + 1 }; public: CircularQueue() { } /** * @brief Return whether the queue is empty. * * @return true The queue is empty. * @return false The queue is not empty. */ bool is_empty() { return m_tail.load() == m_head.load(); } /** * @brief Push a value onto the queue. * * @param value The value to push. * @return true The operation succeeded. * @return false The queue was full or someone else was trying to push a value at the same time. */ bool try_push(const T& value) { usize current_tail = m_tail.load(MemoryOrder::Relaxed); const usize new_tail = (current_tail + 1) % Capacity; if (new_tail == m_head.load(MemoryOrder::Acquire)) { // Queue is full return false; } m_data[current_tail] = value; if (!m_tail.compare_exchange_strong(current_tail, new_tail, MemoryOrder::Release, MemoryOrder::Relaxed)) { // Someone else updated the tail return false; } return true; } /** * @brief Pop a value from the queue. * * @param value The variable to store the value into. * @return true The operation succeeded. * @return false The queue was empty or someone else was trying to pop a value at the same time. */ bool try_pop(T& value) { usize current_head = m_head.load(MemoryOrder::Relaxed); if (current_head == m_tail.load(MemoryOrder::Acquire)) { // Queue is empty return false; } value = m_data[current_head]; const usize new_head = (current_head + 1) % Capacity; if (!m_head.compare_exchange_strong(current_head, new_head, MemoryOrder::Release, MemoryOrder::Relaxed)) { // Someone else updated the head return false; } return true; } private: T m_data[Capacity]; Atomic m_head = 0; Atomic m_tail = 0; }; /** * @brief An atomic lock-free circular FIFO queue. * * In this variant the size is set at runtime. * * @tparam T The type of data to store in this queue. */ template class DynamicCircularQueue { public: DynamicCircularQueue() { } ~DynamicCircularQueue() { if (m_data) free_impl(m_data); } /** * @brief Return whether the queue is empty. * * @return true The queue is empty. * @return false The queue is not empty. */ bool is_empty() { return m_tail.load() == m_head.load(); } /** * @brief Set the size of the queue and allocate memory for it. * * This should not be used to grow the queue, as all existing data is lost. In most cases, this function will only * be called once to set the initial size of the queue and that's it. * * @param size The amount of elements to make space for. * @return Result Whether the operation succeeded. */ Result set_size(usize size) { if (m_data) free_impl(m_data); m_data = (T*)TRY(calloc_impl(size + 1, sizeof(T), false)); m_capacity = size + 1; return {}; } /** * @brief Push a value onto the queue. * * @param value The value to push. * @return true The operation succeeded. * @return false The queue was full or someone else was trying to push a value at the same time. */ bool try_push(const T& value) { check(m_capacity); usize current_tail = m_tail.load(MemoryOrder::Relaxed); const usize new_tail = (current_tail + 1) % m_capacity; if (new_tail == m_head.load(MemoryOrder::Acquire)) { // Queue is full return false; } m_data[current_tail] = value; if (!m_tail.compare_exchange_strong(current_tail, new_tail, MemoryOrder::Release, MemoryOrder::Relaxed)) { // Someone else updated the tail return false; } return true; } /** * @brief Pop a value from the queue. * * @param value The variable to store the value into. * @return true The operation succeeded. * @return false The queue was empty or someone else was trying to pop a value at the same time. */ bool try_pop(T& value) { check(m_capacity); usize current_head = m_head.load(MemoryOrder::Relaxed); if (current_head == m_tail.load(MemoryOrder::Acquire)) { // Queue is empty return false; } value = m_data[current_head]; const usize new_head = (current_head + 1) % m_capacity; if (!m_head.compare_exchange_strong(current_head, new_head, MemoryOrder::Release, MemoryOrder::Relaxed)) { // Someone else updated the head return false; } return true; } private: T* m_data = nullptr; usize m_capacity = 0; Atomic m_head = 0; Atomic m_tail = 0; };