From 875dd695eaf918cf7c1919b0beb0b984a90ed6dc Mon Sep 17 00:00:00 2001 From: aryanchakravorty Date: Fri, 13 Mar 2026 19:17:35 +0530 Subject: [PATCH 01/30] updated defs.hpp locklfree_spsc_unbounded --- include/lockfree_spsc_unbounded/defs.hpp | 47 ++++++++++++++++++++++-- include/utils.hpp | 32 ++++++++-------- 2 files changed, 60 insertions(+), 19 deletions(-) diff --git a/include/lockfree_spsc_unbounded/defs.hpp b/include/lockfree_spsc_unbounded/defs.hpp index 5968a33..6e40502 100644 --- a/include/lockfree_spsc_unbounded/defs.hpp +++ b/include/lockfree_spsc_unbounded/defs.hpp @@ -11,19 +11,30 @@ template class lockfree_spsc_unbounded { // Works exactly same as the blocking_mpmc_unbounded queue (see this once) // with tail pointer pointing to stub node and your head pointer updates as // per the pushes. See the Lockless_Node in utils to understand the working. + // Note that the next pointers are atomic there. Why ?? [Reason this] + //--------------------My understanding:------------------ + //if head and tail point to same stub node ,sp in trying to modify next ptr,sc is trying to read + // next ptr(to check if queue is empty) + // Also the head and tail members are cache-aligned. Why ?? [Reason this] (ask // me for details) + //-------------------My understanding:------------------- + //cpu update on common cache line,if sp makes some changes,in invalidates other cores cache,so this has to keep on updating + //which is time consuming + //cache aligning make the pointers sit on different cache lines,so now we can spam modifications(very latent) // [Copy of blocking_mpmc_unbounded] // For the implementation, we start with a stub node and both head and tail - // are initialized to it. When we push, we make a new stub node, move the data - // into the current tail and then change the tail to the new stub. We have two - // methods : wait_and_pop() which waits on the queue and returns element & + // are initialized to it. + //When we push, we make a new stub node, move the data + // into the current tail and then change the tail to the new stub. + //We have two methods : wait_and_pop() which waits on the queue and returns element & // try_pop() which returns an element if queue is not empty otherwise returns // some neutral element OR a false boolean whichever is applicable. Pop works // by returning the data stored in head node and replacing head to its next // node. We handle the empty queue gracefully as per the pop type. + private: using node = tsfqueue::__utils::Lockless_Node; @@ -31,6 +42,14 @@ template class lockfree_spsc_unbounded { // 1. node* head; // 2. node* tail; + //------------------Note:-------------------- + //we do not need atomic head/tail due to spsc + //doing cache align so that head and tail are in different caches + //we do not use shared pointers due to reference counter bottlneck-we are keeping the pointers light weight + + alignas(tsfq::__impl::cache_line_size)node * head; + alignas(tsfq::__impl::cache_line_size)node *tail; + // Description of priavte members : // 1. node* head -> Pointer to the head node // 2. node* tail -> Pointer to tail node @@ -38,7 +57,29 @@ template class lockfree_spsc_unbounded { public: // Public member functions : + // Add relevant constructors and destructors -> Add these here only + //-----------Constructor------------- + //using memory_order_relaxed because default ordering is memory_order_seq_cst + lockfree_spsc_unbounded(){ + node* stub= new node(); + stub->next.store(nullptr,std::memory_order_relaxed); + + head=stub; + tail=stub; + } + //-----------Destructor--------------- + ~lockfree_spsc_unbounded(){ + while(head!=nullptr){ + + node* current=head; + head=head->next.load(std::memory_order_relaxed); + delete current; + + } + } + + // 1. void push(value) : Pushes the value inside the queue, copies the value // 2. void wait_and_pop(value ref) : Blocking wait on queue, returns value in // the reference passed as parameter diff --git a/include/utils.hpp b/include/utils.hpp index de434c9..385cb72 100644 --- a/include/utils.hpp +++ b/include/utils.hpp @@ -1,18 +1,18 @@ -#include -#include + #include + #include -namespace tsfqueue::__utils { -template struct Node { - std::shared_ptr data; - std::unique_ptr> next; -}; -template struct Lockless_Node { - T data; - std::atomic next; -} -} // namespace tsfqueue::__utils + namespace tsfqueue::__utils { + template struct Node { + std::shared_ptr data; + std::unique_ptr> next; + }; + template struct Lockless_Node { + T data; + std::atomic next; + }; + } // namespace tsfqueue::__utils -namespace tsfq::__impl { -static constexpr size_t cache_line_size = - std::hardware_destructive_interference_size; -} \ No newline at end of file + namespace tsfq::__impl { + static constexpr size_t cache_line_size = + std::hardware_destructive_interference_size; + } \ No newline at end of file From e60b642c28c1eda0a858c9ea906c00da6677de99 Mon Sep 17 00:00:00 2001 From: aryanchakravorty Date: Sun, 15 Mar 2026 02:48:03 +0530 Subject: [PATCH 02/30] Added few functions to impl.hpp --- include/lockfree_spsc_unbounded/defs.hpp | 25 ++++++++++-- include/lockfree_spsc_unbounded/impl.hpp | 49 ++++++++++++++++++++--- include/lockfree_spsc_unbounded/queue.hpp | 2 +- 3 files changed, 66 insertions(+), 10 deletions(-) diff --git a/include/lockfree_spsc_unbounded/defs.hpp b/include/lockfree_spsc_unbounded/defs.hpp index 6e40502..9be5773 100644 --- a/include/lockfree_spsc_unbounded/defs.hpp +++ b/include/lockfree_spsc_unbounded/defs.hpp @@ -15,12 +15,12 @@ template class lockfree_spsc_unbounded { // Note that the next pointers are atomic there. Why ?? [Reason this] //--------------------My understanding:------------------ //if head and tail point to same stub node ,sp in trying to modify next ptr,sc is trying to read - // next ptr(to check if queue is empty) + // next ptr(to check if queue is empty)->race condition // Also the head and tail members are cache-aligned. Why ?? [Reason this] (ask // me for details) //-------------------My understanding:------------------- - //cpu update on common cache line,if sp makes some changes,in invalidates other cores cache,so this has to keep on updating + //cpu update on common cache line,if sp makes some changes,in invalidates other cores(sc) cache,so this has to keep on updating //which is time consuming //cache aligning make the pointers sit on different cache lines,so now we can spam modifications(very latent) @@ -44,7 +44,7 @@ template class lockfree_spsc_unbounded { //------------------Note:-------------------- //we do not need atomic head/tail due to spsc - //doing cache align so that head and tail are in different caches + //doing cache align so that head and tail are in different cache lines //we do not use shared pointers due to reference counter bottlneck-we are keeping the pointers light weight alignas(tsfq::__impl::cache_line_size)node * head; @@ -75,24 +75,41 @@ template class lockfree_spsc_unbounded { node* current=head; head=head->next.load(std::memory_order_relaxed); delete current; - + } } // 1. void push(value) : Pushes the value inside the queue, copies the value + void push(T); + // 2. void wait_and_pop(value ref) : Blocking wait on queue, returns value in // the reference passed as parameter + void wait_and_pop(T&); + // 3. bool try_pop(value ref) : Returns true and // gives the value in reference passed, false otherwise + bool try_pop(T&); + // 4. bool empty() : Returns // whether the queue is empty or not at that instant + bool empty(); + // 5. bool peek(value ref) : Returns the front/top element of queue in ref (false if empty queue) + bool peek(T&); + // 6. Add static asserts + // 7. Add emplace_back using perfect forwarding and variadic templates (you // can use this in push then) + template + void emplace_back(Args&&... args); + // 8. Add size() function + int size(); + // 9. Any more suggestions ?? + // 10. Why no shared_ptr ?? [Reason this] }; } // namespace tsfqueue::__impl diff --git a/include/lockfree_spsc_unbounded/impl.hpp b/include/lockfree_spsc_unbounded/impl.hpp index 8d737a9..bda8586 100644 --- a/include/lockfree_spsc_unbounded/impl.hpp +++ b/include/lockfree_spsc_unbounded/impl.hpp @@ -3,23 +3,62 @@ #include "defs.hpp" + template using queue = tsfqueue::__impl::lockfree_spsc_unbounded; -template void queue::push(T value) {} +template void queue::push(T value) { + //std::move casts it as rvalue-no cost of copying + emplace_back(std::move(value)); +} + +template bool queue::try_pop(T &value) { + node* new_head = head->next.load(std::memory_order_acquire); + + if(new_head == nullptr){ + return false; + } + + node* old_head = head; + value = std::move(head->data); + head = new_head; + + delete old_head; + + return true; +} + +template void queue::wait_and_pop(T &value) { + +} -template bool queue::try_pop(T &value) {} +template bool queue::peek(T &value) { -template void queue::wait_and_pop(T &value) {} +} -template bool queue::peek(T &value) {} +template bool queue::empty(void) { -template bool queue::empty(void) {} +} + +template +template +void queue::emplace_back(Args&&... args){ + node* stub = new node(); + stub->next.store(nullptr,std::memory_order_relaxed); + + tail->data = T(std::forward(args)...); + tail->next.store(stub,std::memory_order_release); + + tail = stub; +} #endif // 1. Add static asserts + // 2. Add emplace_back using perfect forwarding and variadic templates (you // can use this in push then) + // 3. Add size() function + // 4. Any more suggestions ?? \ No newline at end of file diff --git a/include/lockfree_spsc_unbounded/queue.hpp b/include/lockfree_spsc_unbounded/queue.hpp index 5c1eaf3..f5eb963 100644 --- a/include/lockfree_spsc_unbounded/queue.hpp +++ b/include/lockfree_spsc_unbounded/queue.hpp @@ -3,5 +3,5 @@ #include "defs.hpp" #include "impl.hpp" - +using namespace tsfqueue::__impl; #endif \ No newline at end of file From 141138d4e2c51a0ea297b227fb04357598e05d76 Mon Sep 17 00:00:00 2001 From: aryanchakravorty Date: Sun, 15 Mar 2026 15:42:10 +0530 Subject: [PATCH 03/30] Added wait_and_pop,might be wrong --- include/lockfree_spsc_unbounded/defs.hpp | 37 +++++++++++++++++------- include/lockfree_spsc_unbounded/impl.hpp | 20 ++++++++++++- 2 files changed, 46 insertions(+), 11 deletions(-) diff --git a/include/lockfree_spsc_unbounded/defs.hpp b/include/lockfree_spsc_unbounded/defs.hpp index 9be5773..4266d61 100644 --- a/include/lockfree_spsc_unbounded/defs.hpp +++ b/include/lockfree_spsc_unbounded/defs.hpp @@ -5,6 +5,7 @@ #include #include #include +#include namespace tsfqueue::__impl { template class lockfree_spsc_unbounded { @@ -12,23 +13,28 @@ template class lockfree_spsc_unbounded { // with tail pointer pointing to stub node and your head pointer updates as // per the pushes. See the Lockless_Node in utils to understand the working. + //-------------------------------------------------------------------------- // Note that the next pointers are atomic there. Why ?? [Reason this] - //--------------------My understanding:------------------ + // //if head and tail point to same stub node ,sp in trying to modify next ptr,sc is trying to read // next ptr(to check if queue is empty)->race condition + //-------------------------------------------------------------------------- - // Also the head and tail members are cache-aligned. Why ?? [Reason this] (ask - // me for details) - //-------------------My understanding:------------------- + //------------------------------------------------------------------------------ + // Also the head and tail members are cache-aligned. Why ?? [Reason this] (ask me for details) + // //cpu update on common cache line,if sp makes some changes,in invalidates other cores(sc) cache,so this has to keep on updating //which is time consuming //cache aligning make the pointers sit on different cache lines,so now we can spam modifications(very latent) + //-------------------------------------------------------------------------------- // [Copy of blocking_mpmc_unbounded] // For the implementation, we start with a stub node and both head and tail // are initialized to it. + //When we push, we make a new stub node, move the data // into the current tail and then change the tail to the new stub. + //We have two methods : wait_and_pop() which waits on the queue and returns element & // try_pop() which returns an element if queue is not empty otherwise returns // some neutral element OR a false boolean whichever is applicable. Pop works @@ -45,7 +51,6 @@ template class lockfree_spsc_unbounded { //------------------Note:-------------------- //we do not need atomic head/tail due to spsc //doing cache align so that head and tail are in different cache lines - //we do not use shared pointers due to reference counter bottlneck-we are keeping the pointers light weight alignas(tsfq::__impl::cache_line_size)node * head; alignas(tsfq::__impl::cache_line_size)node *tail; @@ -55,18 +60,21 @@ template class lockfree_spsc_unbounded { // 2. node* tail -> Pointer to tail node // 3. Cache align 1-2 + + public: // Public member functions : + //----------------------------------------------------------------------------------- // Add relevant constructors and destructors -> Add these here only + //-----------Constructor------------- //using memory_order_relaxed because default ordering is memory_order_seq_cst lockfree_spsc_unbounded(){ - node* stub= new node(); - stub->next.store(nullptr,std::memory_order_relaxed); + node* head= new node(); + head->next.store(nullptr,std::memory_order_relaxed); - head=stub; - tail=stub; + tail=head; } //-----------Destructor--------------- ~lockfree_spsc_unbounded(){ @@ -79,6 +87,15 @@ template class lockfree_spsc_unbounded { } } + //Delete Copy and Move constructor + //Copy constructor not req as its spsc/could also cause double free crash + //move constructor can break the queue if thread is running and we move the queue somewhere else + + lockfree_spsc_unbounded(const lockfree_spsc_unbounded&)=delete; + lockfree_spsc_unbounded &operator=(const lockfree_spsc_unbounded &)=delete; + + //--------------------------------------------------------------------------------------------- + // 1. void push(value) : Pushes the value inside the queue, copies the value void push(T); @@ -106,7 +123,7 @@ template class lockfree_spsc_unbounded { void emplace_back(Args&&... args); // 8. Add size() function - int size(); + int64_t size(); // 9. Any more suggestions ?? diff --git a/include/lockfree_spsc_unbounded/impl.hpp b/include/lockfree_spsc_unbounded/impl.hpp index bda8586..5a2cae6 100644 --- a/include/lockfree_spsc_unbounded/impl.hpp +++ b/include/lockfree_spsc_unbounded/impl.hpp @@ -30,6 +30,16 @@ template bool queue::try_pop(T &value) { template void queue::wait_and_pop(T &value) { + head->next.wait(nullptr,std::memory_order_acquire); + + node* new_head = head->next.load(std::memory_order_relaxed); + node* old_head = head; + value = std::move(head->data); + + head = new_head; + + delete old_head; + } template bool queue::peek(T &value) { @@ -37,7 +47,7 @@ template bool queue::peek(T &value) { } template bool queue::empty(void) { - + return (head->next.load(std::memory_order_acquire)==nullptr); } template @@ -49,9 +59,17 @@ void queue::emplace_back(Args&&... args){ tail->data = T(std::forward(args)...); tail->next.store(stub,std::memory_order_release); + //wakes up any sleeping thread + tail->next.notify_one(); + tail = stub; } +template +int64_t queue::size(){ + +} + #endif // 1. Add static asserts From f3df3612b382d011f4bc2c3f78e600021b01a545 Mon Sep 17 00:00:00 2001 From: aryanchakravorty Date: Sun, 15 Mar 2026 16:10:12 +0530 Subject: [PATCH 04/30] Initial implementation of impl.hpp --- include/lockfree_spsc_unbounded/impl.hpp | 123 ++++++++++++----------- 1 file changed, 66 insertions(+), 57 deletions(-) diff --git a/include/lockfree_spsc_unbounded/impl.hpp b/include/lockfree_spsc_unbounded/impl.hpp index 5a2cae6..2520704 100644 --- a/include/lockfree_spsc_unbounded/impl.hpp +++ b/include/lockfree_spsc_unbounded/impl.hpp @@ -1,82 +1,91 @@ -#ifndef LOCKFREE_SPSC_UNBOUNDED_IMPL -#define LOCKFREE_SPSC_UNBOUNDED_IMPL + #ifndef LOCKFREE_SPSC_UNBOUNDED_IMPL + #define LOCKFREE_SPSC_UNBOUNDED_IMPL -#include "defs.hpp" + #include "defs.hpp" -template -using queue = tsfqueue::__impl::lockfree_spsc_unbounded; + template + using queue = tsfqueue::__impl::lockfree_spsc_unbounded; -template void queue::push(T value) { - //std::move casts it as rvalue-no cost of copying - emplace_back(std::move(value)); -} + template void queue::push(T value) { + //std::move casts it as rvalue-no cost of copying + emplace_back(std::move(value)); + } -template bool queue::try_pop(T &value) { - node* new_head = head->next.load(std::memory_order_acquire); + template bool queue::try_pop(T &value) { + node* new_head = head->next.load(std::memory_order_acquire); - if(new_head == nullptr){ - return false; - } + if(new_head == nullptr){ + return false; + } - node* old_head = head; - value = std::move(head->data); - head = new_head; - - delete old_head; + node* old_head = head; + value = std::move(head->data); + head = new_head; + + delete old_head; - return true; -} + capacity.fetch_sub(1,std::memory_order_relaxed); -template void queue::wait_and_pop(T &value) { - - head->next.wait(nullptr,std::memory_order_acquire); + return true; + } - node* new_head = head->next.load(std::memory_order_relaxed); - node* old_head = head; - value = std::move(head->data); - - head = new_head; + template void queue::wait_and_pop(T &value) { + + head->next.wait(nullptr,std::memory_order_acquire); - delete old_head; - -} + node* new_head = head->next.load(std::memory_order_relaxed); + node* old_head = head; + value = std::move(head->data); + + head = new_head; -template bool queue::peek(T &value) { + delete old_head; -} + capacity.fetch_sub(1,std::memory_order_relaxed); + + } -template bool queue::empty(void) { - return (head->next.load(std::memory_order_acquire)==nullptr); -} + template bool queue::peek(T &value) { + if(empty()){ + return false; + } + value = head -> data; + return true; + } -template -template -void queue::emplace_back(Args&&... args){ - node* stub = new node(); - stub->next.store(nullptr,std::memory_order_relaxed); + template bool queue::empty(void) { + return (head->next.load(std::memory_order_acquire)==nullptr); + } - tail->data = T(std::forward(args)...); - tail->next.store(stub,std::memory_order_release); + template + template + void queue::emplace_back(Args&&... args){ + node* stub = new node(); + stub->next.store(nullptr,std::memory_order_relaxed); - //wakes up any sleeping thread - tail->next.notify_one(); + tail->data = T(std::forward(args)...); + capacity.fetch_add(1,std::memory_order_relaxed); + tail->next.store(stub,std::memory_order_release); - tail = stub; -} + //wakes up one sleeping thread + tail->next.notify_one(); -template -int64_t queue::size(){ + tail = stub; + } -} + template + size_t queue::size(){ + return capacity.load(std::memory_order_relaxed); + } -#endif + #endif -// 1. Add static asserts + // 1. Add static asserts -// 2. Add emplace_back using perfect forwarding and variadic templates (you -// can use this in push then) + // 2. Add emplace_back using perfect forwarding and variadic templates (you + // can use this in push then) -// 3. Add size() function + // 3. Add size() function -// 4. Any more suggestions ?? \ No newline at end of file + // 4. Any more suggestions ?? \ No newline at end of file From f3443768cbf77131e5c66113c47cadd4ca9392e5 Mon Sep 17 00:00:00 2001 From: rviz Date: Mon, 16 Mar 2026 03:49:46 +0530 Subject: [PATCH 05/30] Added constructor, basic try_push, try_pop, size implementation --- include/lockfree_spsc_bounded/defs.hpp | 121 ++++++++++++++----------- include/lockfree_spsc_bounded/impl.hpp | 55 ++++++++++- 2 files changed, 120 insertions(+), 56 deletions(-) diff --git a/include/lockfree_spsc_bounded/defs.hpp b/include/lockfree_spsc_bounded/defs.hpp index 0ca12e5..9590b77 100644 --- a/include/lockfree_spsc_bounded/defs.hpp +++ b/include/lockfree_spsc_bounded/defs.hpp @@ -1,62 +1,79 @@ #ifndef LOCKFREE_SPSC_BOUNDED_DEFS #define LOCKFREE_SPSC_BOUNDED_DEFS -#include "utils.hpp" +#include "../utils.hpp" #include #include #include -namespace tsfqueue::__impl { -template class lockfree_spsc_bounded { - // For the implementation, we first take the size of the bounded queue from - // user inside the templates so that we can do compile time memory allocation. - // We have two atomic pointer, head and tail, tail for pushing the element and - // head for popping. We also add check tail == head for empty which means one - // redundant element during allocation. We keep head_cache and tail_cache as - // cached copies to have a cache efficient code (discuss with me for details). - // All the data members are cache aligned to prevent cache-line bouncing. - // The user is provided with both set of functions : try_pop() and try_push() - // for a wait-free code And wait_and_pop() and wait_and_push() for a lock-less - // code but not wait-free variant. Thus, the user is given a choice to choose - // among the preferred endpoints as per use case. -private: - // Add the private members : - // std::atomic head; - // std::atomic tail; - // size_t head_cache; - // size_t tail_cache; - // T arr[]; - // static constexpr size_t capacity; - - // Description of private members : - // 1. std::atomic head is the atomic head pointer - // 2. std::atomic tail is the atomic tail pointer - // 3. size_t head_cache is the cached head pointer - // 4. size_t tail_cache is the cached tail pointer - // 5. T arr[] compile time allocated array - // Cache align 1-5. - // 6. static constexpr size_t capcity to store the capcity for operations in - // functions Why static ?? Why constexpr ?? [Reason this] - -public: - // Public Member functions : - // Add appropriate constructors and destructors -> Add here only - // 1. void wait_and_push(value) : Busy wait until element is pushed - // 2. bool try_push(value) : Try to push if not full else leave (returns false - // if could not push else true) - // 3. void wait_and_pop(value ref) : Busy wait until we have atmost 1 elt and - // then pop it and store in reference - // 4. bool try_pop(value ref) : Try to pop and return false if failed bool - // 5. empty(void) : Checks if the queue is empty and return bool - // 6. bool peek(value ref) : Peek the top of the queue. - // Will work only in SPSC/MPSC why ?? [Reason this] - // 7. Add static asserts - // 8. Add emplace_back using perfect forwarding and variadic templates (you - // can use this in push then) - // 9. Add size() function - // 10. Any more suggestions ?? - // 11. Why no shared_ptr ?? [Reason this] -}; +namespace tsfqueue::__impl +{ + template + class lockfree_spsc_bounded + { + // For the implementation, we first take the size of the bounded queue from + // user inside the templates so that we can do compile time memory allocation. + // We have two atomic pointer, head and tail, tail for pushing the element and + // head for popping. We also add check tail == head for empty which means one + // redundant element during allocation. We keep head_cache and tail_cache as + // cached copies to have a cache efficient code (discuss with me for details). + // All the data members are cache aligned to prevent cache-line bouncing. + // The user is provided with both set of functions : try_pop() and try_push() + // for a wait-free code And wait_and_pop() and wait_and_push() for a lock-less + // code but not wait-free variant. Thus, the user is given a choice to choose + // among the preferred endpoints as per use case. + private: + // Add the private members : + alignas(tsfq::__impl::cache_line_size) std::atomic head; + alignas(tsfq::__impl::cache_line_size) std::atomic tail; + alignas(tsfq::__impl::cache_line_size) size_t head_cache; + alignas(tsfq::__impl::cache_line_size) size_t tail_cache; + T arr[Capacity]; + static constexpr size_t capacity = Capacity; + + // Description of private members : + // 1. std::atomic head is the atomic head pointer + // 2. std::atomic tail is the atomic tail pointer + // 3. size_t head_cache is the cached head pointer + // 4. size_t tail_cache is the cached tail pointer + // 5. T arr[] compile time allocated array + // Cache align 1-5. + // 6. static constexpr size_t capcity to store the capcity for operations in + // functions Why static ?? Why constexpr ?? [Reason this] + + public: + // Public Member functions : + // Add appropriate constructors and destructors -> Add here only + lockfree_spsc_bounded() : head(0), tail(0), head_cache(0), tail_cache(0) {} + + // 1. void wait_and_push(value) : Busy wait until element is pushed + void wait_and_push(T); + + // 2. bool try_push(value) : Try to push if not full else leave (returns false + // if could not push else true) + bool try_push(T); + + // 3. void wait_and_pop(value ref) : Busy wait until we have atmost 1 elt and + void wait_and_pop(T &); + // then pop it and store in reference + // 4. bool try_pop(value ref) : Try to pop and return false if failed bool + bool try_pop(T &); + // 5. empty(void) : Checks if the queue is empty and return bool + bool empty(); + // 6. bool peek(value ref) : Peek the top of the queue. + bool peek(T &); + + template + bool emplace_back(Args&&... args); + size_t size(); + // Will work only in SPSC/MPSC why ?? [Reason this] + // 7. Add static asserts + // 8. Add emplace_back using perfect forwarding and variadic templates (you + // can use this in push then) + // 9. Add size() function + // 10. Any more suggestions ?? + // 11. Why no shared_ptr ?? [Reason this] + }; } // namespace tsfqueue::__impl #endif \ No newline at end of file diff --git a/include/lockfree_spsc_bounded/impl.hpp b/include/lockfree_spsc_bounded/impl.hpp index 9a0b36f..1188c61 100644 --- a/include/lockfree_spsc_bounded/impl.hpp +++ b/include/lockfree_spsc_bounded/impl.hpp @@ -2,18 +2,38 @@ #define LOCKFREE_SPSC_BOUNDED_IMPL_CT #include "defs.hpp" +#include template using queue = tsfqueue::__impl::lockfree_spsc_bounded; template -void queue::wait_and_push(T value) {} +void queue::wait_and_push(T value) +{ +} template -bool queue::try_push(T value) {} +bool queue::try_push(T value) +{ + return emplace_back(std::move(value)); +} template -bool queue::try_pop(T &value) {} +bool queue::try_pop(T &value) +{ + if (tail_cache == head_cache) + { + tail_cache = tail.load(std::memory_order_acquire); // refresh cache + if (tail_cache == head_cache) // empty + { + return false; + } + } + value = arr[head_cache % Capacity]; + head_cache++; + head.store(head_cache, std::memory_order_relaxed); + return true; +} template void queue::wait_and_pop(T &value) {} @@ -21,7 +41,34 @@ void queue::wait_and_pop(T &value) {} template bool queue::peek(T &value) {} -template bool queue::empty() {} +template +bool queue::empty() {} + +template +template +bool queue::emplace_back(Args&&... args) +{ + if (tail_cache - head_cache == Capacity) + { + head_cache = head.load(std::memory_order_relaxed); // refresh cache + if (tail_cache - head_cache == Capacity) // full + { + return false; + } + } + arr[tail_cache % Capacity] = T(std::forward(args)...); + tail_cache++; + tail.store(tail_cache, std::memory_order_release); + return true; +} + +template +size_t queue::size() +{ + size_t t = tail.load(std::memory_order_relaxed); + size_t h = head.load(std::memory_order_relaxed); + return t - h; +} #endif From d5f0e53b9dc46b0744d22a21066eba6373ab9399 Mon Sep 17 00:00:00 2001 From: aryanchakravorty Date: Mon, 16 Mar 2026 11:51:07 +0530 Subject: [PATCH 06/30] Fixed wait_and_pop and fixed some other errors --- include/lockfree_spsc_unbounded/defs.hpp | 11 ++++++--- include/lockfree_spsc_unbounded/impl.hpp | 30 +++++++++++++----------- 2 files changed, 24 insertions(+), 17 deletions(-) diff --git a/include/lockfree_spsc_unbounded/defs.hpp b/include/lockfree_spsc_unbounded/defs.hpp index 4266d61..967821f 100644 --- a/include/lockfree_spsc_unbounded/defs.hpp +++ b/include/lockfree_spsc_unbounded/defs.hpp @@ -60,7 +60,8 @@ template class lockfree_spsc_unbounded { // 2. node* tail -> Pointer to tail node // 3. Cache align 1-2 - + //capacity + alignas(tsfq::__impl::cache_line_size)std::atomiccapacity {0}; public: // Public member functions : @@ -71,7 +72,7 @@ template class lockfree_spsc_unbounded { //-----------Constructor------------- //using memory_order_relaxed because default ordering is memory_order_seq_cst lockfree_spsc_unbounded(){ - node* head= new node(); + head= new node(); head->next.store(nullptr,std::memory_order_relaxed); tail=head; @@ -94,6 +95,9 @@ template class lockfree_spsc_unbounded { lockfree_spsc_unbounded(const lockfree_spsc_unbounded&)=delete; lockfree_spsc_unbounded &operator=(const lockfree_spsc_unbounded &)=delete; + lockfree_spsc_unbounded(lockfree_spsc_unbounded&&) = delete; + lockfree_spsc_unbounded &operator=(lockfree_spsc_unbounded&&) = delete; + //--------------------------------------------------------------------------------------------- @@ -123,11 +127,12 @@ template class lockfree_spsc_unbounded { void emplace_back(Args&&... args); // 8. Add size() function - int64_t size(); + size_t size(); // 9. Any more suggestions ?? // 10. Why no shared_ptr ?? [Reason this] + // }; } // namespace tsfqueue::__impl diff --git a/include/lockfree_spsc_unbounded/impl.hpp b/include/lockfree_spsc_unbounded/impl.hpp index 2520704..c19bc76 100644 --- a/include/lockfree_spsc_unbounded/impl.hpp +++ b/include/lockfree_spsc_unbounded/impl.hpp @@ -3,16 +3,19 @@ #include "defs.hpp" + //---------------------------------------- + //compiler was not able to find defintions using this alias + // template + // using queue = tsfqueue::__impl::lockfree_spsc_unbounded; + //---------------------------------------- - template - using queue = tsfqueue::__impl::lockfree_spsc_unbounded; - template void queue::push(T value) { + template void tsfqueue::__impl::lockfree_spsc_unbounded::push(T value) { //std::move casts it as rvalue-no cost of copying emplace_back(std::move(value)); } - template bool queue::try_pop(T &value) { + template bool tsfqueue::__impl::lockfree_spsc_unbounded::try_pop(T &value) { node* new_head = head->next.load(std::memory_order_acquire); if(new_head == nullptr){ @@ -30,9 +33,11 @@ return true; } - template void queue::wait_and_pop(T &value) { + template void tsfqueue::__impl::lockfree_spsc_unbounded::wait_and_pop(T &value) { - head->next.wait(nullptr,std::memory_order_acquire); + while(head->next.load(std::memory_order_acquire) == nullptr){ + std::this_thread::yield();//low latency-high cpu usage + } node* new_head = head->next.load(std::memory_order_relaxed); node* old_head = head; @@ -46,7 +51,7 @@ } - template bool queue::peek(T &value) { + template bool tsfqueue::__impl::lockfree_spsc_unbounded::peek(T &value) { if(empty()){ return false; } @@ -54,13 +59,13 @@ return true; } - template bool queue::empty(void) { - return (head->next.load(std::memory_order_acquire)==nullptr); + template bool tsfqueue::__impl::lockfree_spsc_unbounded::empty(void) { + return (head->next.load(std::memory_order_acquire) == nullptr); } template template - void queue::emplace_back(Args&&... args){ + void tsfqueue::__impl::lockfree_spsc_unbounded::emplace_back(Args&&... args){ node* stub = new node(); stub->next.store(nullptr,std::memory_order_relaxed); @@ -68,14 +73,11 @@ capacity.fetch_add(1,std::memory_order_relaxed); tail->next.store(stub,std::memory_order_release); - //wakes up one sleeping thread - tail->next.notify_one(); - tail = stub; } template - size_t queue::size(){ + size_t tsfqueue::__impl::lockfree_spsc_unbounded::size(){ return capacity.load(std::memory_order_relaxed); } From 298de796e8b2740c9a022df9d3fa0528f610c201 Mon Sep 17 00:00:00 2001 From: abhiraj-singh-154 Date: Mon, 16 Mar 2026 18:11:37 +0530 Subject: [PATCH 07/30] wait_and_push,wait_and_pop updated. --- include/lockfree_spsc_bounded/impl.hpp | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/include/lockfree_spsc_bounded/impl.hpp b/include/lockfree_spsc_bounded/impl.hpp index 1188c61..1179cf0 100644 --- a/include/lockfree_spsc_bounded/impl.hpp +++ b/include/lockfree_spsc_bounded/impl.hpp @@ -10,6 +10,17 @@ using queue = tsfqueue::__impl::lockfree_spsc_bounded; template void queue::wait_and_push(T value) { + while(true){ + if((tail_cache+1)%Capacity==head_cache){ + head_cache=head.load(); + } + else{ + arr[tail_cache]=value; + tail_cache=(tail_cache+1)%Capacity; + tail.store(tail_cache); + break; + } + } } template @@ -36,7 +47,19 @@ bool queue::try_pop(T &value) } template -void queue::wait_and_pop(T &value) {} +void queue::wait_and_pop(T &value) { + while(true){ + if(head_cache==tail_cache){ + tail_cache=tail.load(); + } + else{ + value=arr[head_cache]; + head_cache=(head_cache+1)%Capacity; + head.store(head_cache); + break; + } + } +} template bool queue::peek(T &value) {} From dc7f2e8c7e322fd4962b9cf7e441b6b69f8564ed Mon Sep 17 00:00:00 2001 From: aryanchakravorty Date: Tue, 17 Mar 2026 19:39:26 +0530 Subject: [PATCH 08/30] Added static asserts --- include/lockfree_spsc_unbounded/defs.hpp | 2 ++ include/lockfree_spsc_unbounded/impl.hpp | 18 ++++++++++++++---- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/include/lockfree_spsc_unbounded/defs.hpp b/include/lockfree_spsc_unbounded/defs.hpp index 967821f..2cbea94 100644 --- a/include/lockfree_spsc_unbounded/defs.hpp +++ b/include/lockfree_spsc_unbounded/defs.hpp @@ -6,6 +6,8 @@ #include #include #include +#include + namespace tsfqueue::__impl { template class lockfree_spsc_unbounded { diff --git a/include/lockfree_spsc_unbounded/impl.hpp b/include/lockfree_spsc_unbounded/impl.hpp index c19bc76..56ef38a 100644 --- a/include/lockfree_spsc_unbounded/impl.hpp +++ b/include/lockfree_spsc_unbounded/impl.hpp @@ -6,16 +6,20 @@ //---------------------------------------- //compiler was not able to find defintions using this alias // template - // using queue = tsfqueue::__impl::lockfree_spsc_unbounded; + // using queue = typename tsfqueue::__impl::lockfree_spsc_unbounded; //---------------------------------------- template void tsfqueue::__impl::lockfree_spsc_unbounded::push(T value) { //std::move casts it as rvalue-no cost of copying + static_assert(std::is_copy_constructible::value, + "T must be copy constructible"); emplace_back(std::move(value)); } - template bool tsfqueue::__impl::lockfree_spsc_unbounded::try_pop(T &value) { + template bool tsfqueue::__impl::lockfree_spsc_unbounded::try_pop(T &value) { + static_assert(std::is_nothrow_destructible::value, + "T must be nothrow destructible"); node* new_head = head->next.load(std::memory_order_acquire); if(new_head == nullptr){ @@ -34,7 +38,9 @@ } template void tsfqueue::__impl::lockfree_spsc_unbounded::wait_and_pop(T &value) { - + static_assert(std::is_nothrow_destructible::value, + "T must be nothrow destructible"); + while(head->next.load(std::memory_order_acquire) == nullptr){ std::this_thread::yield();//low latency-high cpu usage } @@ -65,7 +71,11 @@ template template - void tsfqueue::__impl::lockfree_spsc_unbounded::emplace_back(Args&&... args){ + void tsfqueue::__impl::lockfree_spsc_unbounded::emplace_back(Args&&... args) + { + static_assert(std::is_constructible::value, + "T must be constructible with Args&&..."); + node* stub = new node(); stub->next.store(nullptr,std::memory_order_relaxed); From eda00f5655b2a047ba0191174e6e29a79312cbf7 Mon Sep 17 00:00:00 2001 From: AbhigyanSinha466 Date: Tue, 17 Mar 2026 21:28:32 +0530 Subject: [PATCH 09/30] Added peek and empty logic --- include/lockfree_spsc_bounded/impl.hpp | 105 +++++++++++++++---------- 1 file changed, 63 insertions(+), 42 deletions(-) diff --git a/include/lockfree_spsc_bounded/impl.hpp b/include/lockfree_spsc_bounded/impl.hpp index 1179cf0..ad68625 100644 --- a/include/lockfree_spsc_bounded/impl.hpp +++ b/include/lockfree_spsc_bounded/impl.hpp @@ -10,13 +10,16 @@ using queue = tsfqueue::__impl::lockfree_spsc_bounded; template void queue::wait_and_push(T value) { - while(true){ - if((tail_cache+1)%Capacity==head_cache){ - head_cache=head.load(); + while (true) + { + if ((tail_cache + 1) % Capacity == head_cache) + { + head_cache = head.load(); } - else{ - arr[tail_cache]=value; - tail_cache=(tail_cache+1)%Capacity; + else + { + arr[tail_cache] = value; + tail_cache = (tail_cache + 1) % Capacity; tail.store(tail_cache); break; } @@ -26,35 +29,39 @@ void queue::wait_and_push(T value) template bool queue::try_push(T value) { - return emplace_back(std::move(value)); + return emplace_back(std::move(value)); } template bool queue::try_pop(T &value) { - if (tail_cache == head_cache) + if (tail_cache == head_cache) + { + tail_cache = tail.load(std::memory_order_acquire); // refresh cache + if (tail_cache == head_cache) // empty { - tail_cache = tail.load(std::memory_order_acquire); // refresh cache - if (tail_cache == head_cache) // empty - { - return false; - } + return false; } - value = arr[head_cache % Capacity]; - head_cache++; - head.store(head_cache, std::memory_order_relaxed); - return true; + } + value = arr[head_cache % Capacity]; + head_cache++; + head.store(head_cache, std::memory_order_relaxed); + return true; } template -void queue::wait_and_pop(T &value) { - while(true){ - if(head_cache==tail_cache){ - tail_cache=tail.load(); +void queue::wait_and_pop(T &value) +{ + while (true) + { + if (head_cache == tail_cache) + { + tail_cache = tail.load(); } - else{ - value=arr[head_cache]; - head_cache=(head_cache+1)%Capacity; + else + { + value = arr[head_cache]; + head_cache = (head_cache + 1) % Capacity; head.store(head_cache); break; } @@ -62,35 +69,49 @@ void queue::wait_and_pop(T &value) { } template -bool queue::peek(T &value) {} +bool queue::peek(T &value) +{ + size_t cur_head = head.load(); + if (cur_head == tail_cache) + { + tail_cache = tail.load(); + if (tail_cache == head) + return false; + } + value = arr[cur_head]; + return true; +} template -bool queue::empty() {} +bool queue::empty() +{ + return head.load() == tail.load(); +} template -template -bool queue::emplace_back(Args&&... args) +template +bool queue::emplace_back(Args &&...args) { - if (tail_cache - head_cache == Capacity) + if (tail_cache - head_cache == Capacity) + { + head_cache = head.load(std::memory_order_relaxed); // refresh cache + if (tail_cache - head_cache == Capacity) // full { - head_cache = head.load(std::memory_order_relaxed); // refresh cache - if (tail_cache - head_cache == Capacity) // full - { - return false; - } + return false; } - arr[tail_cache % Capacity] = T(std::forward(args)...); - tail_cache++; - tail.store(tail_cache, std::memory_order_release); - return true; + } + arr[tail_cache % Capacity] = T(std::forward(args)...); + tail_cache++; + tail.store(tail_cache, std::memory_order_release); + return true; } template -size_t queue::size() +size_t queue::size() { - size_t t = tail.load(std::memory_order_relaxed); - size_t h = head.load(std::memory_order_relaxed); - return t - h; + size_t t = tail.load(std::memory_order_relaxed); + size_t h = head.load(std::memory_order_relaxed); + return t - h; } #endif From 66b2926404f91facf7f81426d5020e6931399fe2 Mon Sep 17 00:00:00 2001 From: rviz Date: Wed, 18 Mar 2026 00:09:01 +0530 Subject: [PATCH 10/30] consistent head/tail convention --- include/lockfree_spsc_bounded/impl.hpp | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/include/lockfree_spsc_bounded/impl.hpp b/include/lockfree_spsc_bounded/impl.hpp index ad68625..2bcd704 100644 --- a/include/lockfree_spsc_bounded/impl.hpp +++ b/include/lockfree_spsc_bounded/impl.hpp @@ -43,9 +43,9 @@ bool queue::try_pop(T &value) return false; } } - value = arr[head_cache % Capacity]; - head_cache++; - head.store(head_cache, std::memory_order_relaxed); + value = arr[head_cache]; + head_cache = (head_cache + 1) % Capacity; + head.store(head_cache, std::memory_order_release); return true; } @@ -75,7 +75,7 @@ bool queue::peek(T &value) if (cur_head == tail_cache) { tail_cache = tail.load(); - if (tail_cache == head) + if (cur_head == tail_cache) return false; } value = arr[cur_head]; @@ -92,16 +92,16 @@ template template bool queue::emplace_back(Args &&...args) { - if (tail_cache - head_cache == Capacity) + if ((tail_cache + 1) % Capacity == head_cache) { - head_cache = head.load(std::memory_order_relaxed); // refresh cache - if (tail_cache - head_cache == Capacity) // full + head_cache = head.load(std::memory_order_acquire); // refresh cache + if ((tail_cache + 1) % Capacity == head_cache) // full { return false; } } - arr[tail_cache % Capacity] = T(std::forward(args)...); - tail_cache++; + arr[tail_cache] = T(std::forward(args)...); + tail_cache = (tail_cache + 1) % Capacity; tail.store(tail_cache, std::memory_order_release); return true; } @@ -109,9 +109,9 @@ bool queue::emplace_back(Args &&...args) template size_t queue::size() { - size_t t = tail.load(std::memory_order_relaxed); - size_t h = head.load(std::memory_order_relaxed); - return t - h; + size_t t = tail.load(std::memory_order_acquire); + size_t h = head.load(std::memory_order_acquire); + return (t - h + Capacity) % Capacity; } #endif From 176da76329c733f3bd84a9f61db2a051bc0c4ef6 Mon Sep 17 00:00:00 2001 From: AbhigyanSinha466 Date: Wed, 18 Mar 2026 09:35:47 +0530 Subject: [PATCH 11/30] Added memory ordering in peek and empty , and fixed capacity =Capacity +1 --- include/lockfree_spsc_bounded/defs.hpp | 10 ++++++---- include/lockfree_spsc_bounded/impl.hpp | 24 ++++++++++++------------ 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/include/lockfree_spsc_bounded/defs.hpp b/include/lockfree_spsc_bounded/defs.hpp index 9590b77..3e8702f 100644 --- a/include/lockfree_spsc_bounded/defs.hpp +++ b/include/lockfree_spsc_bounded/defs.hpp @@ -28,8 +28,9 @@ namespace tsfqueue::__impl alignas(tsfq::__impl::cache_line_size) std::atomic tail; alignas(tsfq::__impl::cache_line_size) size_t head_cache; alignas(tsfq::__impl::cache_line_size) size_t tail_cache; - T arr[Capacity]; - static constexpr size_t capacity = Capacity; + static constexpr size_t capacity = Capacity + 1; + alignas(tsfq::__impl::cache_line_size) T arr[capacity]; + // aligned the start of the array too // Description of private members : // 1. std::atomic head is the atomic head pointer @@ -45,7 +46,7 @@ namespace tsfqueue::__impl // Public Member functions : // Add appropriate constructors and destructors -> Add here only lockfree_spsc_bounded() : head(0), tail(0), head_cache(0), tail_cache(0) {} - + ~lockfree_spsc_bounded() = default; // 1. void wait_and_push(value) : Busy wait until element is pushed void wait_and_push(T); @@ -64,7 +65,7 @@ namespace tsfqueue::__impl bool peek(T &); template - bool emplace_back(Args&&... args); + bool emplace_back(Args &&...args); size_t size(); // Will work only in SPSC/MPSC why ?? [Reason this] // 7. Add static asserts @@ -72,6 +73,7 @@ namespace tsfqueue::__impl // can use this in push then) // 9. Add size() function // 10. Any more suggestions ?? + // can make the functions peek , empty and size const // 11. Why no shared_ptr ?? [Reason this] }; } // namespace tsfqueue::__impl diff --git a/include/lockfree_spsc_bounded/impl.hpp b/include/lockfree_spsc_bounded/impl.hpp index 2bcd704..3f472ca 100644 --- a/include/lockfree_spsc_bounded/impl.hpp +++ b/include/lockfree_spsc_bounded/impl.hpp @@ -12,14 +12,14 @@ void queue::wait_and_push(T value) { while (true) { - if ((tail_cache + 1) % Capacity == head_cache) + if ((tail_cache + 1) % capacity == head_cache) { head_cache = head.load(); } else { arr[tail_cache] = value; - tail_cache = (tail_cache + 1) % Capacity; + tail_cache = (tail_cache + 1) % capacity; tail.store(tail_cache); break; } @@ -44,7 +44,7 @@ bool queue::try_pop(T &value) } } value = arr[head_cache]; - head_cache = (head_cache + 1) % Capacity; + head_cache = (head_cache + 1) % capacity; head.store(head_cache, std::memory_order_release); return true; } @@ -61,7 +61,7 @@ void queue::wait_and_pop(T &value) else { value = arr[head_cache]; - head_cache = (head_cache + 1) % Capacity; + head_cache = (head_cache + 1) % capacity; head.store(head_cache); break; } @@ -71,11 +71,11 @@ void queue::wait_and_pop(T &value) template bool queue::peek(T &value) { - size_t cur_head = head.load(); + size_t cur_head = head.load(std::memory_order_relaxed); if (cur_head == tail_cache) { - tail_cache = tail.load(); - if (cur_head == tail_cache) + tail_cache = tail.load(std::memory_order_acquire); + if (tail_cache == head) return false; } value = arr[cur_head]; @@ -85,23 +85,23 @@ bool queue::peek(T &value) template bool queue::empty() { - return head.load() == tail.load(); + return head.load(std::memory_order_acquire) == tail.load(std::memory_order_acquire); } template template bool queue::emplace_back(Args &&...args) { - if ((tail_cache + 1) % Capacity == head_cache) + if ((tail_cache + 1) % capacity == head_cache) { head_cache = head.load(std::memory_order_acquire); // refresh cache - if ((tail_cache + 1) % Capacity == head_cache) // full + if ((tail_cache + 1) % capacity == head_cache) // full { return false; } } arr[tail_cache] = T(std::forward(args)...); - tail_cache = (tail_cache + 1) % Capacity; + tail_cache = (tail_cache + 1) % capacity; tail.store(tail_cache, std::memory_order_release); return true; } @@ -111,7 +111,7 @@ size_t queue::size() { size_t t = tail.load(std::memory_order_acquire); size_t h = head.load(std::memory_order_acquire); - return (t - h + Capacity) % Capacity; + return (t - h + capacity) % capacity; } #endif From 42bc20d7a6745281324800bedefa9a53f8b8c6e4 Mon Sep 17 00:00:00 2001 From: abhiraj-singh-154 Date: Wed, 18 Mar 2026 18:38:54 +0530 Subject: [PATCH 12/30] wait and push ,pop memory order added to impl.hpp --- include/lockfree_spsc_bounded/impl.hpp | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/include/lockfree_spsc_bounded/impl.hpp b/include/lockfree_spsc_bounded/impl.hpp index 3f472ca..32dc556 100644 --- a/include/lockfree_spsc_bounded/impl.hpp +++ b/include/lockfree_spsc_bounded/impl.hpp @@ -10,17 +10,18 @@ using queue = tsfqueue::__impl::lockfree_spsc_bounded; template void queue::wait_and_push(T value) { + size_t next_tail = (tail_cache + 1) % capacity; while (true) { - if ((tail_cache + 1) % capacity == head_cache) + if (next_tail == head_cache) { - head_cache = head.load(); + head_cache = head.load(std::memory_order_acquire); } else { arr[tail_cache] = value; - tail_cache = (tail_cache + 1) % capacity; - tail.store(tail_cache); + tail_cache = next_tail; + tail.store(tail_cache, std::memory_order_release); break; } } @@ -56,13 +57,13 @@ void queue::wait_and_pop(T &value) { if (head_cache == tail_cache) { - tail_cache = tail.load(); + tail_cache = tail.load(std::memory_order_acquire); } else { value = arr[head_cache]; head_cache = (head_cache + 1) % capacity; - head.store(head_cache); + head.store(head_cache , std::memory_order_release); break; } } From ccd9959c76c8449e0572123fc91f412fe5d85c04 Mon Sep 17 00:00:00 2001 From: aryanchakravorty Date: Wed, 18 Mar 2026 22:58:10 +0530 Subject: [PATCH 13/30] Fixed static assert,removed comments --- include/lockfree_spsc_unbounded/defs.hpp | 32 +++++++++---------- include/lockfree_spsc_unbounded/impl.hpp | 40 +++++++++++++++++------- 2 files changed, 42 insertions(+), 30 deletions(-) diff --git a/include/lockfree_spsc_unbounded/defs.hpp b/include/lockfree_spsc_unbounded/defs.hpp index 2cbea94..3bf8787 100644 --- a/include/lockfree_spsc_unbounded/defs.hpp +++ b/include/lockfree_spsc_unbounded/defs.hpp @@ -18,16 +18,16 @@ template class lockfree_spsc_unbounded { //-------------------------------------------------------------------------- // Note that the next pointers are atomic there. Why ?? [Reason this] // - //if head and tail point to same stub node ,sp in trying to modify next ptr,sc is trying to read + //if head and tail point to same stub node ,producer is trying to modify next ptr,consumer is trying to read // next ptr(to check if queue is empty)->race condition //-------------------------------------------------------------------------- //------------------------------------------------------------------------------ // Also the head and tail members are cache-aligned. Why ?? [Reason this] (ask me for details) // - //cpu update on common cache line,if sp makes some changes,in invalidates other cores(sc) cache,so this has to keep on updating + //cpu update on common cache line,if producer makes some changes,it invalidates other cores(consumers) cache,so this has to keep on updating //which is time consuming - //cache aligning make the pointers sit on different cache lines,so now we can spam modifications(very latent) + //cache aligning make the pointers sit on different cache lines,so now we can spam modifications //-------------------------------------------------------------------------------- // [Copy of blocking_mpmc_unbounded] @@ -50,14 +50,10 @@ template class lockfree_spsc_unbounded { // 1. node* head; // 2. node* tail; - //------------------Note:-------------------- - //we do not need atomic head/tail due to spsc - //doing cache align so that head and tail are in different cache lines - alignas(tsfq::__impl::cache_line_size)node * head; alignas(tsfq::__impl::cache_line_size)node *tail; - // Description of priavte members : + // Description of private members : // 1. node* head -> Pointer to the head node // 2. node* tail -> Pointer to tail node // 3. Cache align 1-2 @@ -68,30 +64,33 @@ template class lockfree_spsc_unbounded { public: // Public member functions : - //----------------------------------------------------------------------------------- + // Add relevant constructors and destructors -> Add these here only - //-----------Constructor------------- - //using memory_order_relaxed because default ordering is memory_order_seq_cst lockfree_spsc_unbounded(){ + + static_assert(std::is_default_constructible_v, + "T must be default constructible"); + head= new node(); head->next.store(nullptr,std::memory_order_relaxed); tail=head; } - //-----------Destructor--------------- + ~lockfree_spsc_unbounded(){ - while(head!=nullptr){ + static_assert(std::is_nothrow_destructible_v, + "T must be nothrow destructible"); + while(head!=nullptr){ node* current=head; head=head->next.load(std::memory_order_relaxed); delete current; - } } //Delete Copy and Move constructor - //Copy constructor not req as its spsc/could also cause double free crash + //Copy constructor not required as its spsc,could also cause double free crash //move constructor can break the queue if thread is running and we move the queue somewhere else lockfree_spsc_unbounded(const lockfree_spsc_unbounded&)=delete; @@ -100,9 +99,6 @@ template class lockfree_spsc_unbounded { lockfree_spsc_unbounded(lockfree_spsc_unbounded&&) = delete; lockfree_spsc_unbounded &operator=(lockfree_spsc_unbounded&&) = delete; - //--------------------------------------------------------------------------------------------- - - // 1. void push(value) : Pushes the value inside the queue, copies the value void push(T); diff --git a/include/lockfree_spsc_unbounded/impl.hpp b/include/lockfree_spsc_unbounded/impl.hpp index 56ef38a..15f5bb6 100644 --- a/include/lockfree_spsc_unbounded/impl.hpp +++ b/include/lockfree_spsc_unbounded/impl.hpp @@ -11,15 +11,20 @@ template void tsfqueue::__impl::lockfree_spsc_unbounded::push(T value) { - //std::move casts it as rvalue-no cost of copying - static_assert(std::is_copy_constructible::value, - "T must be copy constructible"); + + static_assert(std::is_move_constructible_v, + "T must be move constructible"); + emplace_back(std::move(value)); } template bool tsfqueue::__impl::lockfree_spsc_unbounded::try_pop(T &value) { - static_assert(std::is_nothrow_destructible::value, + + static_assert(std::is_nothrow_destructible_v, "T must be nothrow destructible"); + static_assert(std::is_move_assignable_v, + "T must be move-assignable"); + node* new_head = head->next.load(std::memory_order_acquire); if(new_head == nullptr){ @@ -29,7 +34,7 @@ node* old_head = head; value = std::move(head->data); head = new_head; - + delete old_head; capacity.fetch_sub(1,std::memory_order_relaxed); @@ -38,17 +43,19 @@ } template void tsfqueue::__impl::lockfree_spsc_unbounded::wait_and_pop(T &value) { - static_assert(std::is_nothrow_destructible::value, - "T must be nothrow destructible"); + + static_assert(std::is_nothrow_destructible_v, + "T must be nothrow destructible"); + static_assert(std::is_move_assignable_v, + "T must be move-assignable"); - while(head->next.load(std::memory_order_acquire) == nullptr){ + node * new_head; + while( ( new_head = head->next.load(std::memory_order_acquire) ) == nullptr ){ std::this_thread::yield();//low latency-high cpu usage } - node* new_head = head->next.load(std::memory_order_relaxed); node* old_head = head; value = std::move(head->data); - head = new_head; delete old_head; @@ -58,9 +65,14 @@ } template bool tsfqueue::__impl::lockfree_spsc_unbounded::peek(T &value) { + + static_assert(std::is_copy_assignable_v, + "T must be copy-assignable"); + if(empty()){ return false; } + value = head -> data; return true; } @@ -73,8 +85,12 @@ template void tsfqueue::__impl::lockfree_spsc_unbounded::emplace_back(Args&&... args) { - static_assert(std::is_constructible::value, - "T must be constructible with Args&&..."); + static_assert(std::is_constructible_v, + "T must be constructible with Args&&..."); + static_assert(std::is_default_constructible_v, + "T must be default constructible"); + static_assert(std::is_move_assignable_v, + "T must be move-assignable"); node* stub = new node(); stub->next.store(nullptr,std::memory_order_relaxed); From 904a22fbd2ced67a1eb7e6c90a2f5015d707de55 Mon Sep 17 00:00:00 2001 From: aryanchakravorty Date: Thu, 19 Mar 2026 00:56:06 +0530 Subject: [PATCH 14/30] fixed alias --- include/lockfree_spsc_unbounded/impl.hpp | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/include/lockfree_spsc_unbounded/impl.hpp b/include/lockfree_spsc_unbounded/impl.hpp index 15f5bb6..8bc61e8 100644 --- a/include/lockfree_spsc_unbounded/impl.hpp +++ b/include/lockfree_spsc_unbounded/impl.hpp @@ -3,14 +3,11 @@ #include "defs.hpp" - //---------------------------------------- - //compiler was not able to find defintions using this alias - // template - // using queue = typename tsfqueue::__impl::lockfree_spsc_unbounded; - //---------------------------------------- - - template void tsfqueue::__impl::lockfree_spsc_unbounded::push(T value) { + template + using queue = typename tsfqueue::__impl::lockfree_spsc_unbounded; + + template void queue::push(T value) { static_assert(std::is_move_constructible_v, "T must be move constructible"); @@ -18,7 +15,7 @@ emplace_back(std::move(value)); } - template bool tsfqueue::__impl::lockfree_spsc_unbounded::try_pop(T &value) { + template bool queue::try_pop(T &value) { static_assert(std::is_nothrow_destructible_v, "T must be nothrow destructible"); @@ -42,7 +39,7 @@ return true; } - template void tsfqueue::__impl::lockfree_spsc_unbounded::wait_and_pop(T &value) { + template void queue::wait_and_pop(T &value) { static_assert(std::is_nothrow_destructible_v, "T must be nothrow destructible"); @@ -64,7 +61,7 @@ } - template bool tsfqueue::__impl::lockfree_spsc_unbounded::peek(T &value) { + template bool queue::peek(T &value) { static_assert(std::is_copy_assignable_v, "T must be copy-assignable"); @@ -77,13 +74,13 @@ return true; } - template bool tsfqueue::__impl::lockfree_spsc_unbounded::empty(void) { + template bool queue::empty(void) { return (head->next.load(std::memory_order_acquire) == nullptr); } template template - void tsfqueue::__impl::lockfree_spsc_unbounded::emplace_back(Args&&... args) + void queue::emplace_back(Args&&... args) { static_assert(std::is_constructible_v, "T must be constructible with Args&&..."); @@ -103,7 +100,7 @@ } template - size_t tsfqueue::__impl::lockfree_spsc_unbounded::size(){ + size_t queue::size(){ return capacity.load(std::memory_order_relaxed); } From 6e2db725821b5d147c4354adba7e3a648afca776 Mon Sep 17 00:00:00 2001 From: aryanchakravorty Date: Thu, 19 Mar 2026 01:10:06 +0530 Subject: [PATCH 15/30] fix indentation --- include/lockfree_spsc_unbounded/impl.hpp | 164 +++++++++++------------ 1 file changed, 82 insertions(+), 82 deletions(-) diff --git a/include/lockfree_spsc_unbounded/impl.hpp b/include/lockfree_spsc_unbounded/impl.hpp index 8bc61e8..2306cda 100644 --- a/include/lockfree_spsc_unbounded/impl.hpp +++ b/include/lockfree_spsc_unbounded/impl.hpp @@ -1,116 +1,116 @@ - #ifndef LOCKFREE_SPSC_UNBOUNDED_IMPL - #define LOCKFREE_SPSC_UNBOUNDED_IMPL +#ifndef LOCKFREE_SPSC_UNBOUNDED_IMPL +#define LOCKFREE_SPSC_UNBOUNDED_IMPL - #include "defs.hpp" +#include "defs.hpp" - template - using queue = typename tsfqueue::__impl::lockfree_spsc_unbounded; - - template void queue::push(T value) { +template +using queue = typename tsfqueue::__impl::lockfree_spsc_unbounded; - static_assert(std::is_move_constructible_v, - "T must be move constructible"); +template void queue::push(T value) { - emplace_back(std::move(value)); - } - - template bool queue::try_pop(T &value) { - - static_assert(std::is_nothrow_destructible_v, - "T must be nothrow destructible"); - static_assert(std::is_move_assignable_v, - "T must be move-assignable"); + static_assert(std::is_move_constructible_v, + "T must be move constructible"); - node* new_head = head->next.load(std::memory_order_acquire); + emplace_back(std::move(value)); +} - if(new_head == nullptr){ - return false; - } +template bool queue::try_pop(T &value) { - node* old_head = head; - value = std::move(head->data); - head = new_head; + static_assert(std::is_nothrow_destructible_v, + "T must be nothrow destructible"); + static_assert(std::is_move_assignable_v, + "T must be move-assignable"); - delete old_head; + node* new_head = head->next.load(std::memory_order_acquire); - capacity.fetch_sub(1,std::memory_order_relaxed); + if(new_head == nullptr){ + return false; + } - return true; - } + node* old_head = head; + value = std::move(head->data); + head = new_head; - template void queue::wait_and_pop(T &value) { + delete old_head; - static_assert(std::is_nothrow_destructible_v, - "T must be nothrow destructible"); - static_assert(std::is_move_assignable_v, - "T must be move-assignable"); - - node * new_head; - while( ( new_head = head->next.load(std::memory_order_acquire) ) == nullptr ){ - std::this_thread::yield();//low latency-high cpu usage - } + capacity.fetch_sub(1,std::memory_order_relaxed); - node* old_head = head; - value = std::move(head->data); - head = new_head; + return true; +} - delete old_head; +template void queue::wait_and_pop(T &value) { - capacity.fetch_sub(1,std::memory_order_relaxed); + static_assert(std::is_nothrow_destructible_v, + "T must be nothrow destructible"); + static_assert(std::is_move_assignable_v, + "T must be move-assignable"); + node * new_head; + while( ( new_head = head->next.load(std::memory_order_acquire) ) == nullptr ){ + std::this_thread::yield();//low latency-high cpu usage } - template bool queue::peek(T &value) { + node* old_head = head; + value = std::move(head->data); + head = new_head; - static_assert(std::is_copy_assignable_v, - "T must be copy-assignable"); + delete old_head; - if(empty()){ - return false; - } + capacity.fetch_sub(1,std::memory_order_relaxed); + +} - value = head -> data; - return true; - } +template bool queue::peek(T &value) { + + static_assert(std::is_copy_assignable_v, + "T must be copy-assignable"); - template bool queue::empty(void) { - return (head->next.load(std::memory_order_acquire) == nullptr); + if(empty()){ + return false; } - template - template - void queue::emplace_back(Args&&... args) - { - static_assert(std::is_constructible_v, - "T must be constructible with Args&&..."); - static_assert(std::is_default_constructible_v, - "T must be default constructible"); - static_assert(std::is_move_assignable_v, - "T must be move-assignable"); + value = head -> data; + return true; +} + +template bool queue::empty(void) { + return (head->next.load(std::memory_order_acquire) == nullptr); +} + +template +template +void queue::emplace_back(Args&&... args) +{ + static_assert(std::is_constructible_v, + "T must be constructible with Args&&..."); + static_assert(std::is_default_constructible_v, + "T must be default constructible"); + static_assert(std::is_move_assignable_v, + "T must be move-assignable"); - node* stub = new node(); - stub->next.store(nullptr,std::memory_order_relaxed); + node* stub = new node(); + stub->next.store(nullptr,std::memory_order_relaxed); - tail->data = T(std::forward(args)...); - capacity.fetch_add(1,std::memory_order_relaxed); - tail->next.store(stub,std::memory_order_release); + tail->data = T(std::forward(args)...); + capacity.fetch_add(1,std::memory_order_relaxed); + tail->next.store(stub,std::memory_order_release); - tail = stub; - } + tail = stub; +} - template - size_t queue::size(){ - return capacity.load(std::memory_order_relaxed); - } +template +size_t queue::size(){ + return capacity.load(std::memory_order_relaxed); +} - #endif +#endif - // 1. Add static asserts +// 1. Add static asserts - // 2. Add emplace_back using perfect forwarding and variadic templates (you - // can use this in push then) +// 2. Add emplace_back using perfect forwarding and variadic templates (you +// can use this in push then) - // 3. Add size() function +// 3. Add size() function - // 4. Any more suggestions ?? \ No newline at end of file +// 4. Any more suggestions ?? \ No newline at end of file From 0c3225798cb6678ae113369c807ed20f37d59a70 Mon Sep 17 00:00:00 2001 From: aryanchakravorty Date: Thu, 19 Mar 2026 02:12:50 +0530 Subject: [PATCH 16/30] removed redundancy --- include/lockfree_spsc_unbounded/defs.hpp | 12 +++++++----- include/lockfree_spsc_unbounded/impl.hpp | 2 +- include/lockfree_spsc_unbounded/queue.hpp | 2 +- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/include/lockfree_spsc_unbounded/defs.hpp b/include/lockfree_spsc_unbounded/defs.hpp index 3bf8787..1e0e834 100644 --- a/include/lockfree_spsc_unbounded/defs.hpp +++ b/include/lockfree_spsc_unbounded/defs.hpp @@ -72,19 +72,21 @@ template class lockfree_spsc_unbounded { static_assert(std::is_default_constructible_v, "T must be default constructible"); - head= new node(); + head = new node(); head->next.store(nullptr,std::memory_order_relaxed); - tail=head; + tail = head; + } ~lockfree_spsc_unbounded(){ + static_assert(std::is_nothrow_destructible_v, "T must be nothrow destructible"); - while(head!=nullptr){ - node* current=head; - head=head->next.load(std::memory_order_relaxed); + while(head != nullptr){ + node* current = head; + head = head->next.load(std::memory_order_relaxed); delete current; } } diff --git a/include/lockfree_spsc_unbounded/impl.hpp b/include/lockfree_spsc_unbounded/impl.hpp index 2306cda..31a3c52 100644 --- a/include/lockfree_spsc_unbounded/impl.hpp +++ b/include/lockfree_spsc_unbounded/impl.hpp @@ -5,7 +5,7 @@ template -using queue = typename tsfqueue::__impl::lockfree_spsc_unbounded; +using queue = tsfqueue::__impl::lockfree_spsc_unbounded; template void queue::push(T value) { diff --git a/include/lockfree_spsc_unbounded/queue.hpp b/include/lockfree_spsc_unbounded/queue.hpp index f5eb963..5c1eaf3 100644 --- a/include/lockfree_spsc_unbounded/queue.hpp +++ b/include/lockfree_spsc_unbounded/queue.hpp @@ -3,5 +3,5 @@ #include "defs.hpp" #include "impl.hpp" -using namespace tsfqueue::__impl; + #endif \ No newline at end of file From 93cc4d3c6b83157a58a306c0c3ede33f873eee62 Mon Sep 17 00:00:00 2001 From: AbhigyanSinha466 Date: Thu, 19 Mar 2026 21:04:24 +0530 Subject: [PATCH 17/30] modified peek slightly --- include/lockfree_spsc_bounded/impl.hpp | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/include/lockfree_spsc_bounded/impl.hpp b/include/lockfree_spsc_bounded/impl.hpp index 32dc556..e95b5e7 100644 --- a/include/lockfree_spsc_bounded/impl.hpp +++ b/include/lockfree_spsc_bounded/impl.hpp @@ -72,14 +72,13 @@ void queue::wait_and_pop(T &value) template bool queue::peek(T &value) { - size_t cur_head = head.load(std::memory_order_relaxed); - if (cur_head == tail_cache) + if (head_cache == tail_cache) { tail_cache = tail.load(std::memory_order_acquire); - if (tail_cache == head) + if (tail_cache == head_cache)//empty return false; } - value = arr[cur_head]; + value = arr[head_cache]; return true; } From 6ecd2dcb3ceb3ca9cdb063234e2efdb546a9ad5e Mon Sep 17 00:00:00 2001 From: abhiraj-singh-154 Date: Fri, 20 Mar 2026 12:53:02 +0530 Subject: [PATCH 18/30] adapt spin,thread::wait() and notify() one added --- include/lockfree_spsc_bounded/impl.hpp | 122 +++++++++++++++++++++---- 1 file changed, 102 insertions(+), 20 deletions(-) diff --git a/include/lockfree_spsc_bounded/impl.hpp b/include/lockfree_spsc_bounded/impl.hpp index e95b5e7..b27505c 100644 --- a/include/lockfree_spsc_bounded/impl.hpp +++ b/include/lockfree_spsc_bounded/impl.hpp @@ -10,21 +10,61 @@ using queue = tsfqueue::__impl::lockfree_spsc_bounded; template void queue::wait_and_push(T value) { - size_t next_tail = (tail_cache + 1) % capacity; - while (true) - { - if (next_tail == head_cache) - { - head_cache = head.load(std::memory_order_acquire); + size_t next_tail = tail_cache + 1; + if (next_tail == capacity) { + next_tail = 0; } - else + + static thread_local int spin_threshold = 100; + const int min_spin = 10, max_spin = 1000; + int spin = 0; + bool spun_success = false; + bool done = false; + while (true) { - arr[tail_cache] = value; + if (next_tail == head_cache) + { + done = true; + head_cache = head.load(std::memory_order_acquire); + if (next_tail == head_cache) + { + if (spin < spin_threshold) { + // Busy-wait + } else if (spin < spin_threshold + 100) { + std::this_thread::yield(); + } else { + head.wait(head_cache, std::memory_order_acquire); + + } + spin++; + continue; + } + } + + // Refresh head_cache before was_empty calculation to ensure correctness + if (!done) + { + head_cache = head.load(std::memory_order_acquire); + } + spun_success = (spin < spin_threshold); + bool was_empty = (head_cache == tail_cache); + arr[tail_cache] = std::move(value); tail_cache = next_tail; tail.store(tail_cache, std::memory_order_release); + + if (was_empty) { + tail.notify_one(); + } break; } - } + + // Adapt spin threshold for next time + int delta =std::max(1, spin/10); + if (spun_success) { + spin_threshold = std::min(spin_threshold + delta, max_spin); + } else { + spin_threshold = std::max(spin_threshold - delta, min_spin); + } } template @@ -53,22 +93,64 @@ bool queue::try_pop(T &value) template void queue::wait_and_pop(T &value) { - while (true) - { - if (head_cache == tail_cache) - { - tail_cache = tail.load(std::memory_order_acquire); - } - else + + + bool done=false; + static thread_local int spin_threshold = 100; + const int min_spin = 10, max_spin = 1000; + int spin = 0; + bool spun_success = false; + bool done = false; + while (true) { + if (head_cache == tail_cache) + { + done = true; + tail_cache = tail.load(std::memory_order_acquire); + if (head_cache == tail_cache) { + if (spin < spin_threshold) { + // Busy-wait + } else if (spin < spin_threshold + 100) { + std::this_thread::yield(); + } else { + tail.wait(tail_cache, std::memory_order_acquire); + + } + ++spin; + continue; + } + } + + // Refresh tail_cache before was_full calculation to ensure correctness + if (!done) { + tail_cache = tail.load(std::memory_order_acquire); + } + spun_success = (spin < spin_threshold); + size_t next_tail = tail_cache + 1; + if (next_tail == capacity) { + next_tail = 0; + } + bool was_full = (next_tail == head_cache); value = arr[head_cache]; - head_cache = (head_cache + 1) % capacity; - head.store(head_cache , std::memory_order_release); + head_cache = head_cache + 1; + if (head_cache == capacity) { + head_cache = 0; + } + head.store(head_cache, std::memory_order_release); + if (was_full) { + head.notify_one(); + } break; } - } -} + // Adapt spin threshold for next time + int delta =std::max(1, spin/10); + if (spun_success) { + spin_threshold = std::min(spin_threshold + delta, max_spin); + } else { + spin_threshold = std::max(spin_threshold - delta, min_spin); + } +} template bool queue::peek(T &value) { From 71e00fe5d72fe534b27c8356871f9e9bea3a312a Mon Sep 17 00:00:00 2001 From: prabh1512 Date: Fri, 20 Mar 2026 22:41:38 +0530 Subject: [PATCH 19/30] added static asserts --- include/lockfree_spsc_bounded/defs.hpp | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/include/lockfree_spsc_bounded/defs.hpp b/include/lockfree_spsc_bounded/defs.hpp index 3e8702f..569a2ff 100644 --- a/include/lockfree_spsc_bounded/defs.hpp +++ b/include/lockfree_spsc_bounded/defs.hpp @@ -1,11 +1,9 @@ #ifndef LOCKFREE_SPSC_BOUNDED_DEFS #define LOCKFREE_SPSC_BOUNDED_DEFS - #include "../utils.hpp" #include #include #include - namespace tsfqueue::__impl { template @@ -31,7 +29,6 @@ namespace tsfqueue::__impl static constexpr size_t capacity = Capacity + 1; alignas(tsfq::__impl::cache_line_size) T arr[capacity]; // aligned the start of the array too - // Description of private members : // 1. std::atomic head is the atomic head pointer // 2. std::atomic tail is the atomic tail pointer @@ -42,6 +39,20 @@ namespace tsfqueue::__impl // 6. static constexpr size_t capcity to store the capcity for operations in // functions Why static ?? Why constexpr ?? [Reason this] + static_assert(Capacity > 0, "queue capacity must be greater than zero"); + + static_assert(Capacity < std::numeric_limits::max() - 1, "prevent overflow"); + + static_assert(std::is_default_constructible::value, "type T must be default constructible for array allocation"); + + static_assert(std::is_move_assignable::value || std::is_copy_assignable::value, "type T must be either move-assignable or copy-assignable"); + + static_assert(std::is_destructible::value, "type T must be destructible"); + + static_assert(std::atomic::is_always_lock_free, "must be lock-free"); + + static_assert(alignof(std::atomic) <= tsfq::__impl::cache_line_size, "cache line size is inefficient"); + public: // Public Member functions : // Add appropriate constructors and destructors -> Add here only @@ -49,11 +60,9 @@ namespace tsfqueue::__impl ~lockfree_spsc_bounded() = default; // 1. void wait_and_push(value) : Busy wait until element is pushed void wait_and_push(T); - // 2. bool try_push(value) : Try to push if not full else leave (returns false // if could not push else true) bool try_push(T); - // 3. void wait_and_pop(value ref) : Busy wait until we have atmost 1 elt and void wait_and_pop(T &); // then pop it and store in reference @@ -63,12 +72,10 @@ namespace tsfqueue::__impl bool empty(); // 6. bool peek(value ref) : Peek the top of the queue. bool peek(T &); - template bool emplace_back(Args &&...args); size_t size(); // Will work only in SPSC/MPSC why ?? [Reason this] - // 7. Add static asserts // 8. Add emplace_back using perfect forwarding and variadic templates (you // can use this in push then) // 9. Add size() function @@ -77,5 +84,4 @@ namespace tsfqueue::__impl // 11. Why no shared_ptr ?? [Reason this] }; } // namespace tsfqueue::__impl - -#endif \ No newline at end of file +#endif From b7a886af5e364698f0a5f4d1e0d794e9ec2af699 Mon Sep 17 00:00:00 2001 From: aryanchakravorty Date: Wed, 25 Mar 2026 16:59:02 +0530 Subject: [PATCH 20/30] Used clang-format --- include/lockfree_spsc_unbounded/defs.hpp | 93 +++++++++-------- include/lockfree_spsc_unbounded/impl.hpp | 124 +++++++++++------------ 2 files changed, 106 insertions(+), 111 deletions(-) diff --git a/include/lockfree_spsc_unbounded/defs.hpp b/include/lockfree_spsc_unbounded/defs.hpp index 1e0e834..c5df674 100644 --- a/include/lockfree_spsc_unbounded/defs.hpp +++ b/include/lockfree_spsc_unbounded/defs.hpp @@ -3,11 +3,10 @@ #include "utils.hpp" #include +#include #include -#include #include -#include - +#include namespace tsfqueue::__impl { template class lockfree_spsc_unbounded { @@ -18,30 +17,35 @@ template class lockfree_spsc_unbounded { //-------------------------------------------------------------------------- // Note that the next pointers are atomic there. Why ?? [Reason this] // - //if head and tail point to same stub node ,producer is trying to modify next ptr,consumer is trying to read + // if head and tail point to same stub node ,producer is trying to modify next + // ptr,consumer is trying to read // next ptr(to check if queue is empty)->race condition //-------------------------------------------------------------------------- //------------------------------------------------------------------------------ - // Also the head and tail members are cache-aligned. Why ?? [Reason this] (ask me for details) + // Also the head and tail members are cache-aligned. Why ?? [Reason this] (ask + // me for details) // - //cpu update on common cache line,if producer makes some changes,it invalidates other cores(consumers) cache,so this has to keep on updating - //which is time consuming - //cache aligning make the pointers sit on different cache lines,so now we can spam modifications + // cpu update on common cache line,if producer makes some changes,it + // invalidates other cores(consumers) cache,so this has to keep on updating + // which is time consuming + // cache aligning make the pointers sit on different cache lines,so now we can + // spam modifications //-------------------------------------------------------------------------------- // [Copy of blocking_mpmc_unbounded] // For the implementation, we start with a stub node and both head and tail - // are initialized to it. + // are initialized to it. - //When we push, we make a new stub node, move the data - // into the current tail and then change the tail to the new stub. + // When we push, we make a new stub node, move the data + // into the current tail and then change the tail to the new stub. - //We have two methods : wait_and_pop() which waits on the queue and returns element & - // try_pop() which returns an element if queue is not empty otherwise returns - // some neutral element OR a false boolean whichever is applicable. Pop works - // by returning the data stored in head node and replacing head to its next - // node. We handle the empty queue gracefully as per the pop type. + // We have two methods : wait_and_pop() which waits on the queue and returns + // element & + // try_pop() which returns an element if queue is not empty otherwise returns + // some neutral element OR a false boolean whichever is applicable. Pop works + // by returning the data stored in head node and replacing head to its next + // node. We handle the empty queue gracefully as per the pop type. private: using node = tsfqueue::__utils::Lockless_Node; @@ -50,81 +54,80 @@ template class lockfree_spsc_unbounded { // 1. node* head; // 2. node* tail; - alignas(tsfq::__impl::cache_line_size)node * head; - alignas(tsfq::__impl::cache_line_size)node *tail; + alignas(tsfq::__impl::cache_line_size) node *head; + alignas(tsfq::__impl::cache_line_size) node *tail; // Description of private members : // 1. node* head -> Pointer to the head node // 2. node* tail -> Pointer to tail node // 3. Cache align 1-2 - //capacity - alignas(tsfq::__impl::cache_line_size)std::atomiccapacity {0}; + // capacity + alignas(tsfq::__impl::cache_line_size) std::atomic capacity{0}; public: // Public member functions : + // static asserts + static_assert(std::is_default_constructible_v, + "T must be default constructible"); + static_assert(std::is_nothrow_destructible_v, + "T must be nothrow destructible"); // Add relevant constructors and destructors -> Add these here only - lockfree_spsc_unbounded(){ - - static_assert(std::is_default_constructible_v, - "T must be default constructible"); + lockfree_spsc_unbounded() { head = new node(); - head->next.store(nullptr,std::memory_order_relaxed); + head->next.store(nullptr, std::memory_order_relaxed); tail = head; - } - ~lockfree_spsc_unbounded(){ - - static_assert(std::is_nothrow_destructible_v, - "T must be nothrow destructible"); + ~lockfree_spsc_unbounded() { - while(head != nullptr){ - node* current = head; + while (head != nullptr) { + node *current = head; head = head->next.load(std::memory_order_relaxed); delete current; } } - //Delete Copy and Move constructor - //Copy constructor not required as its spsc,could also cause double free crash - //move constructor can break the queue if thread is running and we move the queue somewhere else + // Delete Copy and Move constructor + // Copy constructor not required as its spsc,could also cause double free + // crash move constructor can break the queue if thread is running and we move + // the queue somewhere else - lockfree_spsc_unbounded(const lockfree_spsc_unbounded&)=delete; - lockfree_spsc_unbounded &operator=(const lockfree_spsc_unbounded &)=delete; + lockfree_spsc_unbounded(const lockfree_spsc_unbounded &) = delete; + lockfree_spsc_unbounded &operator=(const lockfree_spsc_unbounded &) = delete; - lockfree_spsc_unbounded(lockfree_spsc_unbounded&&) = delete; - lockfree_spsc_unbounded &operator=(lockfree_spsc_unbounded&&) = delete; + lockfree_spsc_unbounded(lockfree_spsc_unbounded &&) = delete; + lockfree_spsc_unbounded &operator=(lockfree_spsc_unbounded &&) = delete; // 1. void push(value) : Pushes the value inside the queue, copies the value void push(T); // 2. void wait_and_pop(value ref) : Blocking wait on queue, returns value in // the reference passed as parameter - void wait_and_pop(T&); + void wait_and_pop(T &); // 3. bool try_pop(value ref) : Returns true and // gives the value in reference passed, false otherwise - bool try_pop(T&); + bool try_pop(T &); // 4. bool empty() : Returns // whether the queue is empty or not at that instant bool empty(); - // 5. bool peek(value ref) : Returns the front/top element of queue in ref (false if empty queue) - bool peek(T&); + // 5. bool peek(value ref) : Returns the front/top element of queue in ref + // (false if empty queue) + bool unsafe_peek(T &); // 6. Add static asserts // 7. Add emplace_back using perfect forwarding and variadic templates (you // can use this in push then) - template - void emplace_back(Args&&... args); + template void emplace_back(Args &&...args); // 8. Add size() function size_t size(); diff --git a/include/lockfree_spsc_unbounded/impl.hpp b/include/lockfree_spsc_unbounded/impl.hpp index 31a3c52..8a1b219 100644 --- a/include/lockfree_spsc_unbounded/impl.hpp +++ b/include/lockfree_spsc_unbounded/impl.hpp @@ -3,105 +3,97 @@ #include "defs.hpp" - template using queue = tsfqueue::__impl::lockfree_spsc_unbounded; template void queue::push(T value) { - static_assert(std::is_move_constructible_v, - "T must be move constructible"); + static_assert(std::is_move_constructible_v, + "T must be move constructible"); - emplace_back(std::move(value)); + emplace_back(std::move(value)); } -template bool queue::try_pop(T &value) { +template bool queue::try_pop(T &value) { - static_assert(std::is_nothrow_destructible_v, - "T must be nothrow destructible"); - static_assert(std::is_move_assignable_v, - "T must be move-assignable"); + static_assert(std::is_nothrow_destructible_v, + "T must be nothrow destructible"); + static_assert(std::is_move_assignable_v, "T must be move-assignable"); - node* new_head = head->next.load(std::memory_order_acquire); + node *new_head = head->next.load(std::memory_order_acquire); - if(new_head == nullptr){ - return false; - } + if (new_head == nullptr) { + return false; + } - node* old_head = head; - value = std::move(head->data); - head = new_head; + node *old_head = head; + value = std::move(head->data); + head = new_head; - delete old_head; + delete old_head; - capacity.fetch_sub(1,std::memory_order_relaxed); + capacity.fetch_sub(1, std::memory_order_relaxed); - return true; -} + return true; +} template void queue::wait_and_pop(T &value) { - static_assert(std::is_nothrow_destructible_v, - "T must be nothrow destructible"); - static_assert(std::is_move_assignable_v, - "T must be move-assignable"); - - node * new_head; - while( ( new_head = head->next.load(std::memory_order_acquire) ) == nullptr ){ - std::this_thread::yield();//low latency-high cpu usage - } + static_assert(std::is_nothrow_destructible_v, + "T must be nothrow destructible"); + static_assert(std::is_move_assignable_v, "T must be move-assignable"); - node* old_head = head; - value = std::move(head->data); - head = new_head; + node *new_head; + while ((new_head = head->next.load(std::memory_order_acquire)) == nullptr) { + std::this_thread::yield(); // low latency-high cpu usage + } - delete old_head; + node *old_head = head; + value = std::move(head->data); + head = new_head; - capacity.fetch_sub(1,std::memory_order_relaxed); - + delete old_head; + + capacity.fetch_sub(1, std::memory_order_relaxed); } -template bool queue::peek(T &value) { +template bool queue::unsafe_peek(T &value) { - static_assert(std::is_copy_assignable_v, - "T must be copy-assignable"); + static_assert(std::is_copy_assignable_v, "T must be copy-assignable"); - if(empty()){ - return false; - } + if (empty()) { + return false; + } - value = head -> data; - return true; + value = head->data; + return true; } template bool queue::empty(void) { - return (head->next.load(std::memory_order_acquire) == nullptr); + return (head->next.load(std::memory_order_acquire) == nullptr); } -template -template -void queue::emplace_back(Args&&... args) -{ - static_assert(std::is_constructible_v, - "T must be constructible with Args&&..."); - static_assert(std::is_default_constructible_v, - "T must be default constructible"); - static_assert(std::is_move_assignable_v, - "T must be move-assignable"); - - node* stub = new node(); - stub->next.store(nullptr,std::memory_order_relaxed); - - tail->data = T(std::forward(args)...); - capacity.fetch_add(1,std::memory_order_relaxed); - tail->next.store(stub,std::memory_order_release); - - tail = stub; +template +template +void queue::emplace_back(Args &&...args) { + static_assert(std::is_constructible_v, + "T must be constructible with Args&&..."); + static_assert(std::is_default_constructible_v, + "T must be default constructible"); + static_assert(std::is_move_assignable_v, "T must be move-assignable"); + + node *stub = new node(); + stub->next.store(nullptr, std::memory_order_relaxed); + + tail->data = T(std::forward(args)...); + capacity.fetch_add(1, std::memory_order_relaxed); + tail->next.store(stub, std::memory_order_release); + + tail = stub; } -template -size_t queue::size(){ - return capacity.load(std::memory_order_relaxed); +template size_t queue::size() { + return capacity.load(std::memory_order_relaxed); } #endif From 0b8cdc971a5c5d1bf83c63af6896decd2e4ac27a Mon Sep 17 00:00:00 2001 From: Ritesh Raj Singh Date: Wed, 25 Mar 2026 21:04:19 +0530 Subject: [PATCH 21/30] fixed notify_one logic --- include/lockfree_spsc_bounded/defs.hpp | 2 +- include/lockfree_spsc_bounded/impl.hpp | 218 ++++++++++++++----------- 2 files changed, 125 insertions(+), 95 deletions(-) diff --git a/include/lockfree_spsc_bounded/defs.hpp b/include/lockfree_spsc_bounded/defs.hpp index 569a2ff..ab78d5e 100644 --- a/include/lockfree_spsc_bounded/defs.hpp +++ b/include/lockfree_spsc_bounded/defs.hpp @@ -23,9 +23,9 @@ namespace tsfqueue::__impl private: // Add the private members : alignas(tsfq::__impl::cache_line_size) std::atomic head; + alignas(tsfq::__impl::cache_line_size) size_t tail_cache; alignas(tsfq::__impl::cache_line_size) std::atomic tail; alignas(tsfq::__impl::cache_line_size) size_t head_cache; - alignas(tsfq::__impl::cache_line_size) size_t tail_cache; static constexpr size_t capacity = Capacity + 1; alignas(tsfq::__impl::cache_line_size) T arr[capacity]; // aligned the start of the array too diff --git a/include/lockfree_spsc_bounded/impl.hpp b/include/lockfree_spsc_bounded/impl.hpp index b27505c..aaf4f05 100644 --- a/include/lockfree_spsc_bounded/impl.hpp +++ b/include/lockfree_spsc_bounded/impl.hpp @@ -10,61 +10,70 @@ using queue = tsfqueue::__impl::lockfree_spsc_bounded; template void queue::wait_and_push(T value) { - size_t next_tail = tail_cache + 1; - if (next_tail == capacity) { - next_tail = 0; - } + size_t next_tail = tail_cache + 1; + if (next_tail == capacity) + { + next_tail = 0; + } - static thread_local int spin_threshold = 100; - const int min_spin = 10, max_spin = 1000; - int spin = 0; - bool spun_success = false; - bool done = false; - while (true) + static thread_local int spin_threshold = 100; + const int min_spin = 10, max_spin = 1000; + int spin = 0; + bool spun_success = false; + bool done = false; + while (true) + { + if (next_tail == head_cache) { + done = true; + head_cache = head.load(std::memory_order_acquire); if (next_tail == head_cache) { - done = true; - head_cache = head.load(std::memory_order_acquire); - if (next_tail == head_cache) + if (spin < spin_threshold) + { + // Busy-wait + } + else if (spin < spin_threshold + 100) { - if (spin < spin_threshold) { - // Busy-wait - } else if (spin < spin_threshold + 100) { - std::this_thread::yield(); - } else { - head.wait(head_cache, std::memory_order_acquire); - - } - spin++; - continue; + std::this_thread::yield(); } + else + { + head.wait(head_cache, std::memory_order_acquire); + } + spin++; + continue; } + } - // Refresh head_cache before was_empty calculation to ensure correctness - if (!done) - { - head_cache = head.load(std::memory_order_acquire); - } - spun_success = (spin < spin_threshold); - bool was_empty = (head_cache == tail_cache); - arr[tail_cache] = std::move(value); - tail_cache = next_tail; - tail.store(tail_cache, std::memory_order_release); - - if (was_empty) { - tail.notify_one(); - } - break; + // Refresh head_cache before was_empty calculation to ensure correctness + if (!done) + { + head_cache = head.load(std::memory_order_acquire); } + spun_success = (spin < spin_threshold); + bool was_empty = (head_cache == tail_cache); + arr[tail_cache] = std::move(value); + tail_cache = next_tail; + tail.store(tail_cache, std::memory_order_release); - // Adapt spin threshold for next time - int delta =std::max(1, spin/10); - if (spun_success) { - spin_threshold = std::min(spin_threshold + delta, max_spin); - } else { - spin_threshold = std::max(spin_threshold - delta, min_spin); + if (was_empty) + { + tail.notify_one(); } + break; + } + + // Adapt spin threshold for next time + int delta = std::max(1, spin / 10); + if (spun_success) + { + spin_threshold = std::min(spin_threshold + delta, max_spin); + } + else + { + spin_threshold = std::max(spin_threshold - delta, min_spin); + } } template @@ -84,80 +93,96 @@ bool queue::try_pop(T &value) return false; } } + bool was_full = (tail_cache + 1) % capacity == head_cache; value = arr[head_cache]; head_cache = (head_cache + 1) % capacity; head.store(head_cache, std::memory_order_release); + if (was_full) + { + head.notify_one(); + } return true; } template void queue::wait_and_pop(T &value) { - - - bool done=false; - static thread_local int spin_threshold = 100; - const int min_spin = 10, max_spin = 1000; - int spin = 0; - bool spun_success = false; - bool done = false; - while (true) + bool done = false; + static thread_local int spin_threshold = 100; + const int min_spin = 10, max_spin = 1000; + int spin = 0; + bool spun_success = false; + bool done = false; + while (true) + { + if (head_cache == tail_cache) { + done = true; + tail_cache = tail.load(std::memory_order_acquire); if (head_cache == tail_cache) { - done = true; - tail_cache = tail.load(std::memory_order_acquire); - if (head_cache == tail_cache) { - if (spin < spin_threshold) { - // Busy-wait - } else if (spin < spin_threshold + 100) { - std::this_thread::yield(); - } else { - tail.wait(tail_cache, std::memory_order_acquire); - - } - ++spin; - continue; + if (spin < spin_threshold) + { + // Busy-wait } + else if (spin < spin_threshold + 100) + { + std::this_thread::yield(); + } + else + { + tail.wait(tail_cache, std::memory_order_acquire); + } + ++spin; + continue; } - - // Refresh tail_cache before was_full calculation to ensure correctness - if (!done) { - tail_cache = tail.load(std::memory_order_acquire); - } - spun_success = (spin < spin_threshold); - size_t next_tail = tail_cache + 1; - if (next_tail == capacity) { - next_tail = 0; - } - bool was_full = (next_tail == head_cache); - value = arr[head_cache]; - head_cache = head_cache + 1; - if (head_cache == capacity) { - head_cache = 0; - } - head.store(head_cache, std::memory_order_release); - if (was_full) { - head.notify_one(); - } - break; } - // Adapt spin threshold for next time - int delta =std::max(1, spin/10); - if (spun_success) { - spin_threshold = std::min(spin_threshold + delta, max_spin); - } else { - spin_threshold = std::max(spin_threshold - delta, min_spin); + // Refresh tail_cache before was_full calculation to ensure correctness + if (!done) + { + tail_cache = tail.load(std::memory_order_acquire); + } + spun_success = (spin < spin_threshold); + size_t next_tail = tail_cache + 1; + if (next_tail == capacity) + { + next_tail = 0; } + bool was_full = (next_tail == head_cache); + value = arr[head_cache]; + head_cache = head_cache + 1; + if (head_cache == capacity) + { + head_cache = 0; + } + head.store(head_cache, std::memory_order_release); + if (was_full) + { + head.notify_one(); + } + break; + } + + // Adapt spin threshold for next time + int delta = std::max(1, spin / 10); + if (spun_success) + { + spin_threshold = std::min(spin_threshold + delta, max_spin); + } + else + { + spin_threshold = std::max(spin_threshold - delta, min_spin); + } } + template bool queue::peek(T &value) { if (head_cache == tail_cache) { tail_cache = tail.load(std::memory_order_acquire); - if (tail_cache == head_cache)//empty + if (tail_cache == head_cache) // empty return false; } value = arr[head_cache]; @@ -182,9 +207,14 @@ bool queue::emplace_back(Args &&...args) return false; } } + bool was_empty = (head_cache == tail_cache); arr[tail_cache] = T(std::forward(args)...); tail_cache = (tail_cache + 1) % capacity; tail.store(tail_cache, std::memory_order_release); + if (was_empty) + { + tail.notify_one(); + } return true; } From 5d7974835cf0a544dc01b300dca24106aa2dd8c1 Mon Sep 17 00:00:00 2001 From: Ritesh Raj Singh Date: Wed, 25 Mar 2026 21:08:07 +0530 Subject: [PATCH 22/30] fixed bug --- include/lockfree_spsc_bounded/impl.hpp | 1 - 1 file changed, 1 deletion(-) diff --git a/include/lockfree_spsc_bounded/impl.hpp b/include/lockfree_spsc_bounded/impl.hpp index aaf4f05..99f29b1 100644 --- a/include/lockfree_spsc_bounded/impl.hpp +++ b/include/lockfree_spsc_bounded/impl.hpp @@ -107,7 +107,6 @@ bool queue::try_pop(T &value) template void queue::wait_and_pop(T &value) { - bool done = false; static thread_local int spin_threshold = 100; const int min_spin = 10, max_spin = 1000; int spin = 0; From f1de787a1f95cf8c0ac605bb73292ac6cc4873fc Mon Sep 17 00:00:00 2001 From: ToshitJain Date: Thu, 2 Apr 2026 12:34:11 +0530 Subject: [PATCH 23/30] Refactoring PRs 1 --- include/blocking_mpmc_unbounded/defs.hpp | 54 ++--- include/blocking_mpmc_unbounded/impl.hpp | 253 +++++++++++------------ include/lockfree_spsc_bounded/defs.hpp | 157 +++++++------- include/lockfree_spsc_bounded/impl.hpp | 122 ++++------- include/lockfree_spsc_unbounded/defs.hpp | 12 +- include/lockfree_spsc_unbounded/impl.hpp | 19 +- include/utils.hpp | 22 +- 7 files changed, 299 insertions(+), 340 deletions(-) diff --git a/include/blocking_mpmc_unbounded/defs.hpp b/include/blocking_mpmc_unbounded/defs.hpp index cc6ec05..9eafb11 100644 --- a/include/blocking_mpmc_unbounded/defs.hpp +++ b/include/blocking_mpmc_unbounded/defs.hpp @@ -2,13 +2,13 @@ #define BLOCKING_MPMC_UNBOUNDED_DEFS #include "../utils.hpp" +#include #include #include #include -#include #include -namespace tsfqueue::__impl { +namespace tsfqueue::impl { template class blocking_mpmc_unbounded { // For the implementation, we start with a stub node and both head and tail // are initialized to it. When we push, we make a new stub node, move the data @@ -19,7 +19,7 @@ template class blocking_mpmc_unbounded { // by returning the data stored in head node and replacing head to its next // node. We handle the empty queue gracefully as per the pop type. private: - using node = tsfqueue::__utils::Node; + using node = tsfqueue::utils::Node; // Add private members : std::mutex head_mutex; @@ -27,7 +27,8 @@ template class blocking_mpmc_unbounded { std::mutex tail_mutex; node *tail; size_t size_q; // newly added -> to maintain size of the queue. - std::mutex size_mutex; // newly added -> lock for accessing or modifying size_q. + std::mutex + size_mutex; // newly added -> lock for accessing or modifying size_q. std::condition_variable cond; // Description of private members : @@ -57,17 +58,19 @@ template class blocking_mpmc_unbounded { std::unique_ptr wait_and_get(); std::unique_ptr try_get(); - std::unique_ptr wait_for_and_get(std::chrono::milliseconds); // Added New + std::unique_ptr + wait_for_and_get(std::chrono::milliseconds); // Added New - // node *get_tail() : Helper function to get normal pointer to tail at a - removed as not used - // particular instant std::unique_ptr wait_and_get() : Helper function to - // blocking wait on unique_ptr of head after popping std::unique_ptr try_get() - // : Helper function to try to get unique_ptr of head after popping + // node *get_tail() : Helper function to get normal pointer to tail at a - + // removed as not used particular instant std::unique_ptr wait_and_get() : + // Helper function to blocking wait on unique_ptr of head after popping + // std::unique_ptr try_get() : Helper function to try to get unique_ptr of + // head after popping public: // Public member functions : // Add relevant constructors and destructors -> Add these here only - blocking_mpmc_unbounded(){ + blocking_mpmc_unbounded() { static_assert(!std::is_reference_v, "Queue cannot store reference types."); @@ -84,26 +87,27 @@ template class blocking_mpmc_unbounded { std::is_destructible_v, "Unable to destroy the queue, as the given type is not destructable."); - while(head) - { + while (head) { head = std::move(head->next); } } - - // Removed Copy constrcutor, because we can't copy this queue, as it has pointers to memory locations. - blocking_mpmc_unbounded(const blocking_mpmc_unbounded& other) = delete; - blocking_mpmc_unbounded& operator=(const blocking_mpmc_unbounded& other) = delete; - + + // Removed Copy constrcutor, because we can't copy this queue, as it has + // pointers to memory locations. + blocking_mpmc_unbounded(const blocking_mpmc_unbounded &other) = delete; + blocking_mpmc_unbounded & + operator=(const blocking_mpmc_unbounded &other) = delete; + // Removed Move constrcutor, because mutexes are not movable. - blocking_mpmc_unbounded(blocking_mpmc_unbounded&& other) = delete; - blocking_mpmc_unbounded& operator=(blocking_mpmc_unbounded&& other) = delete; + blocking_mpmc_unbounded(blocking_mpmc_unbounded &&other) = delete; + blocking_mpmc_unbounded &operator=(blocking_mpmc_unbounded &&other) = delete; // 1. void push(value) : Pushes the value inside the queue, copies the value void push(T); // 2. void wait_and_pop(value ref) : Blocking wait on queue, returns value in // the reference passed as parameter - void wait_and_pop(T&); + void wait_and_pop(T &); // 3. std::shared_ptr wait_and_pop(void) : Blocking wait on queue, returns // value as a shared ptr allocated inside the call @@ -111,7 +115,7 @@ template class blocking_mpmc_unbounded { // 4. bool try_pop(value ref) : Returns true and gives the value in reference // passed, false otherwise - bool try_pop(T&); + bool try_pop(T &); // 5. std::shared_ptr try_pop() : Returns a shared ptr with data, returns // nullptr if failed @@ -124,8 +128,7 @@ template class blocking_mpmc_unbounded { // 8. Add emplace_back using perfect forwarding and variadic templates (you // can use this in push then) - template - void emplace_back(Args&&... args); + template void emplace_back(Args &&...args); // 9. Add size() function size_t size(); @@ -140,14 +143,13 @@ template class blocking_mpmc_unbounded { std::shared_ptr unsafe_peek(); // wait_for_get() added to private section. - bool wait_for_and_pop(T&, std::chrono::milliseconds); + bool wait_for_and_pop(T &, std::chrono::milliseconds); std::shared_ptr wait_for_and_pop(std::chrono::milliseconds); // wait_for_and_get() added to private section. void clear(); // clears all the elements in the queue. - }; -} // namespace tsfqueue::__impl +} // namespace tsfqueue::impl #endif diff --git a/include/blocking_mpmc_unbounded/impl.hpp b/include/blocking_mpmc_unbounded/impl.hpp index 55d8d3f..54c464d 100644 --- a/include/blocking_mpmc_unbounded/impl.hpp +++ b/include/blocking_mpmc_unbounded/impl.hpp @@ -3,13 +3,10 @@ #include "defs.hpp" -template -using queue = tsfqueue::__impl::blocking_mpmc_unbounded; - -template -using node = tsfqueue::__utils::Node; +template using node = tsfqueue::utils::Node; -template void queue::push(T value) { +namespace tsfqueue::impl { +template void blocking_mpmc_unbounded::push(T value) { static_assert(std::is_copy_constructible_ || std::is_move_constructible_v, @@ -38,63 +35,60 @@ template void queue::push(T value) { std::lock_guard guard_size_mutex(size_mutex); size_q++; } // added this scope because if we notify a thread before unlocking - // size_mutex, in - // the wait_and_pop() function we are checking empty() which requires size_mutex. + // size_mutex, in + // the wait_and_pop() function we are checking empty() which requires + // size_mutex. - // Notify any thread (if any) waiting in "wait_and_pop" to wake up and pop. - cond.notify_one(); + // Notify any thread (if any) waiting in "wait_and_pop" to wake up and pop. + cond.notify_one(); - return; + return; } - template -std::unique_ptr::node> queue::wait_and_get() { - //Locking the head mutex - std::unique_lock head_lock(head_mutex); - - //Waiting for the queue to not be empty - cond.wait(head_lock, [this]{ - return !empty(); - }); - - //Extracting the head node and updating it - std::unique_ptr old_head = std::move(head); - head = std::move(old_head->next); - - //Updating the queue size - { - std::lock_guard size_lock(size_mutex); - size_q--; - } - - return std::move(old_head); +std::unique_ptr blocking_mpmc_unbounded::wait_and_get() { + // Locking the head mutex + std::unique_lock head_lock(head_mutex); + + // Waiting for the queue to not be empty + cond.wait(head_lock, [this] { return !empty(); }); + + // Extracting the head node and updating it + std::unique_ptr old_head = std::move(head); + head = std::move(old_head->next); + + // Updating the queue size + { + std::lock_guard size_lock(size_mutex); + size_q--; + } + + return std::move(old_head); } -template -std::unique_ptr::node> queue::try_get() { - std::lock_guard guard_head_mutex(head_mutex); - if (size() > 0){ - std::unique_ptr removing_node = std::move(head); - head = std::move(removing_node->next); +template +std::unique_ptr blocking_mpmc_unbounded::try_get() { + std::lock_guard guard_head_mutex(head_mutex); + if (size() > 0) { + std::unique_ptr removing_node = std::move(head); + head = std::move(removing_node->next); - std::lock_guard guard_size_mutex(size_mutex); - size_q--; + std::lock_guard guard_size_mutex(size_mutex); + size_q--; - return std::move(removing_node); - } - - return nullptr; + return std::move(removing_node); + } + + return nullptr; } -template -size_t queue::size(){ - std::lock_guard lock_size(size_mutex); +template size_t blocking_mpmc_unbounded::size() { + std::lock_guard lock_size(size_mutex); - return size_q; + return size_q; } -template void queue::wait_and_pop(T &value) { +template void blocking_mpmc_unbounded::wait_and_pop(T &value) { static_assert(std::is_copy_assignable_v || std::is_move_assignable_v, "T must be copy-assignable or move-assignable to be popped " @@ -108,17 +102,17 @@ template void queue::wait_and_pop(T &value) { // pointer issue with peek() } -template std::shared_ptr queue::wait_and_pop() { +template +std::shared_ptr blocking_mpmc_unbounded::wait_and_pop() { - //Obtain the popped_node with the help of wait_and_get() - std::unique_ptr popped_node = wait_and_get(); + // Obtain the popped_node with the help of wait_and_get() + std::unique_ptr popped_node = wait_and_get(); - //Return the shared pointer of the data - return popped_node->data; + // Return the shared pointer of the data + return popped_node->data; } -template -bool queue::try_pop(T &value) { +template bool blocking_mpmc_unbounded::try_pop(T &value) { static_assert(std::is_copy_assignable_v || std::is_move_assignable_v, "T must be copy-assignable or move-assignable to be popped " @@ -134,48 +128,50 @@ bool queue::try_pop(T &value) { } } -template -std::shared_ptr queue::try_pop() { - std::unique_ptr removed_node = try_get(); - if (removed_node == nullptr){ - return nullptr; - }else{ - return removed_node->data; - } +template std::shared_ptr blocking_mpmc_unbounded::try_pop() { + std::unique_ptr removed_node = try_get(); + if (removed_node == nullptr) { + return nullptr; + } else { + return removed_node->data; + } } -template bool queue::empty() { - std::lock_guard lock_size(size_mutex); - - return (size_q == 0); +template bool blocking_mpmc_unbounded::empty() { + std::lock_guard lock_size(size_mutex); + + return (size_q == 0); } template template -void queue::emplace_back(Args&&... args){ - // Create a new tail node. - std::unique_ptr new_tail_unique_ptr = std::make_unique(); +void blocking_mpmc_unbounded::emplace_back(Args &&...args) { + // Create a new tail node. + std::unique_ptr new_tail_unique_ptr = std::make_unique(); - // Emplace the data directly at the memory address of shared_ptr. (Perfect forwarding) - std::shared_ptr shared_ptr_for_value = std::make_shared(std::forward(args)...); + // Emplace the data directly at the memory address of shared_ptr. (Perfect + // forwarding) + std::shared_ptr shared_ptr_for_value = + std::make_shared(std::forward(args)...); - // Get exclusive axcess [Do it aftere non-critical tasks] - std::lock_guard guard_tail_mutex(tail_mutex); + // Get exclusive axcess [Do it aftere non-critical tasks] + std::lock_guard guard_tail_mutex(tail_mutex); - tail->data = std::move(shared_ptr_for_value); - tail->next = std::move(new_tail_unique_ptr); + tail->data = std::move(shared_ptr_for_value); + tail->next = std::move(new_tail_unique_ptr); - // change the tail. - tail = tail->next.get(); + // change the tail. + tail = tail->next.get(); - // Increment size [Doing this at the end so that consumer thread does not interfer with this operation.] - std::lock_guard guard_size_mutex(size_mutex); - size_q++; + // Increment size [Doing this at the end so that consumer thread does not + // interfer with this operation.] + std::lock_guard guard_size_mutex(size_mutex); + size_q++; - return; + return; } -template bool queue::unsafe_peek(T &value) { +template bool blocking_mpmc_unbounded::unsafe_peek(T &value) { static_assert(std::is_copy_assignable_v || std::is_move_assignable_v, "T must be copy-assignable or move-assignable to be peeked " @@ -200,7 +196,8 @@ template bool queue::unsafe_peek(T &value) { return 0; } -template std::shared_ptr queue::unsafe_peek() { +template +std::shared_ptr blocking_mpmc_unbounded::unsafe_peek() { // Get exclusive axcess of head. std::lock_guard guard_head_mutex(head_mutex); @@ -221,78 +218,76 @@ template std::shared_ptr queue::unsafe_peek() { } template -std::unique_ptr::node> queue::wait_for_and_get(std::chrono::milliseconds timeout) -{ - // Using unique_lock to lock and unlock on our will. - std::unique_lock lock_head(head_mutex); - - // Waiting at max until timeout ms of time. - if(!cond.wait_for(lock_head,timeout,[this](){ - bool flag=empty(); +std::unique_ptr blocking_mpmc_unbounded::wait_for_and_get( + std::chrono::milliseconds timeout) { + // Using unique_lock to lock and unlock on our will. + std::unique_lock lock_head(head_mutex); + + // Waiting at max until timeout ms of time. + if (!cond.wait_for(lock_head, timeout, [this]() { + bool flag = empty(); return !flag; - })) - { - return nullptr; - } + })) { + return nullptr; + } - std::unique_ptr new_head = std::move(head->next); - std::unique_ptr return_node = std::move(head); + std::unique_ptr new_head = std::move(head->next); + std::unique_ptr return_node = std::move(head); - head = std::move(new_head); + head = std::move(new_head); - { - std::lock_guard lock_size(size_mutex); - size_q--; - } + { + std::lock_guard lock_size(size_mutex); + size_q--; + } - return std::move(return_node); + return std::move(return_node); } template -std::shared_ptr queue::wait_for_and_pop(std::chrono::milliseconds timeout) -{ - std::unique_ptr::node> return_node = std::move(wait_for_and_get(timeout)); - if (return_node == nullptr) - { - return nullptr; - } - - return return_node->data; +std::shared_ptr blocking_mpmc_unbounded::wait_for_and_pop( + std::chrono::milliseconds timeout) { + std::unique_ptr::node> return_node = + std::move(wait_for_and_get(timeout)); + if (return_node == nullptr) { + return nullptr; + } + + return return_node->data; } template -bool queue::wait_for_and_pop(T &value,std::chrono::milliseconds timeout) -{ +bool blocking_mpmc_unbounded::wait_for_and_pop( + T &value, std::chrono::milliseconds timeout) { static_assert(std::is_copy_assignable_v || std::is_move_assignable_v, "T must be copy-assignable or move-assignable to be popped " "into a reference."); - std::unique_ptr::node> return_node = + std::unique_ptr::node> return_node = std::move(wait_for_and_get(timeout)); if (return_node == nullptr) { return false; } - value = *(return_node->data); + value = *(return_node->data); - return true; + return true; } -template -void queue::clear() -{ - std::lock_guard lock_head(head_mutex); - std::lock_guard lock_tail(tail_mutex); +template void blocking_mpmc_unbounded::clear() { + std::lock_guard lock_head(head_mutex); + std::lock_guard lock_tail(tail_mutex); - head = std::make_unique(); - tail = head.get(); + head = std::make_unique(); + tail = head.get(); - std::lock_guard lock_size(size_mutex); - size_q = 0; + std::lock_guard lock_size(size_mutex); + size_q = 0; - return; + return; } +} // namespace tsfqueue::impl #endif diff --git a/include/lockfree_spsc_bounded/defs.hpp b/include/lockfree_spsc_bounded/defs.hpp index ab78d5e..c2b3695 100644 --- a/include/lockfree_spsc_bounded/defs.hpp +++ b/include/lockfree_spsc_bounded/defs.hpp @@ -4,84 +4,85 @@ #include #include #include -namespace tsfqueue::__impl -{ - template - class lockfree_spsc_bounded - { - // For the implementation, we first take the size of the bounded queue from - // user inside the templates so that we can do compile time memory allocation. - // We have two atomic pointer, head and tail, tail for pushing the element and - // head for popping. We also add check tail == head for empty which means one - // redundant element during allocation. We keep head_cache and tail_cache as - // cached copies to have a cache efficient code (discuss with me for details). - // All the data members are cache aligned to prevent cache-line bouncing. - // The user is provided with both set of functions : try_pop() and try_push() - // for a wait-free code And wait_and_pop() and wait_and_push() for a lock-less - // code but not wait-free variant. Thus, the user is given a choice to choose - // among the preferred endpoints as per use case. - private: - // Add the private members : - alignas(tsfq::__impl::cache_line_size) std::atomic head; - alignas(tsfq::__impl::cache_line_size) size_t tail_cache; - alignas(tsfq::__impl::cache_line_size) std::atomic tail; - alignas(tsfq::__impl::cache_line_size) size_t head_cache; - static constexpr size_t capacity = Capacity + 1; - alignas(tsfq::__impl::cache_line_size) T arr[capacity]; - // aligned the start of the array too - // Description of private members : - // 1. std::atomic head is the atomic head pointer - // 2. std::atomic tail is the atomic tail pointer - // 3. size_t head_cache is the cached head pointer - // 4. size_t tail_cache is the cached tail pointer - // 5. T arr[] compile time allocated array - // Cache align 1-5. - // 6. static constexpr size_t capcity to store the capcity for operations in - // functions Why static ?? Why constexpr ?? [Reason this] +namespace tsfqueue::impl { +template class lockfree_spsc_bounded { + // For the implementation, we first take the size of the bounded queue from + // user inside the templates so that we can do compile time memory allocation. + // We have two atomic pointer, head and tail, tail for pushing the element and + // head for popping. We also add check tail == head for empty which means one + // redundant element during allocation. We keep head_cache and tail_cache as + // cached copies to have a cache efficient code (discuss with me for details). + // All the data members are cache aligned to prevent cache-line bouncing. + // The user is provided with both set of functions : try_pop() and try_push() + // for a wait-free code And wait_and_pop() and wait_and_push() for a lock-less + // code but not wait-free variant. Thus, the user is given a choice to choose + // among the preferred endpoints as per use case. +private: + // Add the private members : + alignas(tsfq::__impl::cache_line_size) std::atomic head; + alignas(tsfq::__impl::cache_line_size) size_t tail_cache; + alignas(tsfq::__impl::cache_line_size) std::atomic tail; + alignas(tsfq::__impl::cache_line_size) size_t head_cache; + static constexpr size_t capacity = Capacity + 1; + alignas(tsfq::__impl::cache_line_size) T arr[capacity]; + // aligned the start of the array too + // Description of private members : + // 1. std::atomic head is the atomic head pointer + // 2. std::atomic tail is the atomic tail pointer + // 3. size_t head_cache is the cached head pointer + // 4. size_t tail_cache is the cached tail pointer + // 5. T arr[] compile time allocated array + // Cache align 1-5. + // 6. static constexpr size_t capcity to store the capcity for operations in + // functions Why static ?? Why constexpr ?? [Reason this] - static_assert(Capacity > 0, "queue capacity must be greater than zero"); - - static_assert(Capacity < std::numeric_limits::max() - 1, "prevent overflow"); - - static_assert(std::is_default_constructible::value, "type T must be default constructible for array allocation"); - - static_assert(std::is_move_assignable::value || std::is_copy_assignable::value, "type T must be either move-assignable or copy-assignable"); - - static_assert(std::is_destructible::value, "type T must be destructible"); - - static_assert(std::atomic::is_always_lock_free, "must be lock-free"); - - static_assert(alignof(std::atomic) <= tsfq::__impl::cache_line_size, "cache line size is inefficient"); + static_assert(Capacity > 0, "queue capacity must be greater than zero"); - public: - // Public Member functions : - // Add appropriate constructors and destructors -> Add here only - lockfree_spsc_bounded() : head(0), tail(0), head_cache(0), tail_cache(0) {} - ~lockfree_spsc_bounded() = default; - // 1. void wait_and_push(value) : Busy wait until element is pushed - void wait_and_push(T); - // 2. bool try_push(value) : Try to push if not full else leave (returns false - // if could not push else true) - bool try_push(T); - // 3. void wait_and_pop(value ref) : Busy wait until we have atmost 1 elt and - void wait_and_pop(T &); - // then pop it and store in reference - // 4. bool try_pop(value ref) : Try to pop and return false if failed bool - bool try_pop(T &); - // 5. empty(void) : Checks if the queue is empty and return bool - bool empty(); - // 6. bool peek(value ref) : Peek the top of the queue. - bool peek(T &); - template - bool emplace_back(Args &&...args); - size_t size(); - // Will work only in SPSC/MPSC why ?? [Reason this] - // 8. Add emplace_back using perfect forwarding and variadic templates (you - // can use this in push then) - // 9. Add size() function - // 10. Any more suggestions ?? - // can make the functions peek , empty and size const - // 11. Why no shared_ptr ?? [Reason this] - }; -} // namespace tsfqueue::__impl + static_assert(Capacity < std::numeric_limits::max() - 1, + "prevent overflow"); + + static_assert(std::is_default_constructible::value, + "type T must be default constructible for array allocation"); + + static_assert(std::is_move_assignable::value || + std::is_copy_assignable::value, + "type T must be either move-assignable or copy-assignable"); + + static_assert(std::is_destructible::value, "type T must be destructible"); + + static_assert(std::atomic::is_always_lock_free, "must be lock-free"); + + static_assert(alignof(std::atomic) <= tsfq::__impl::cache_line_size, + "cache line size is inefficient"); + +public: + // Public Member functions : + // Add appropriate constructors and destructors -> Add here only + lockfree_spsc_bounded() : head(0), tail(0), head_cache(0), tail_cache(0) {} + ~lockfree_spsc_bounded() = default; + // 1. void wait_and_push(value) : Busy wait until element is pushed + void wait_and_push(T); + // 2. bool try_push(value) : Try to push if not full else leave (returns false + // if could not push else true) + bool try_push(T); + // 3. void wait_and_pop(value ref) : Busy wait until we have atmost 1 elt and + void wait_and_pop(T &); + // then pop it and store in reference + // 4. bool try_pop(value ref) : Try to pop and return false if failed bool + bool try_pop(T &); + // 5. empty(void) : Checks if the queue is empty and return bool + bool empty(); + // 6. bool peek(value ref) : Peek the top of the queue. + bool peek(T &); + template bool emplace_back(Args &&...args); + size_t size(); + // Will work only in SPSC/MPSC why ?? [Reason this] + // 8. Add emplace_back using perfect forwarding and variadic templates (you + // can use this in push then) + // 9. Add size() function + // 10. Any more suggestions ?? + // can make the functions peek , empty and size const + // 11. Why no shared_ptr ?? [Reason this] +}; +} // namespace tsfqueue::impl #endif diff --git a/include/lockfree_spsc_bounded/impl.hpp b/include/lockfree_spsc_bounded/impl.hpp index 99f29b1..ebc32a7 100644 --- a/include/lockfree_spsc_bounded/impl.hpp +++ b/include/lockfree_spsc_bounded/impl.hpp @@ -4,15 +4,11 @@ #include "defs.hpp" #include +namespace tsfqueue::impl { template -using queue = tsfqueue::__impl::lockfree_spsc_bounded; - -template -void queue::wait_and_push(T value) -{ +void lockfree_spsc_bounded::wait_and_push(T value) { size_t next_tail = tail_cache + 1; - if (next_tail == capacity) - { + if (next_tail == capacity) { next_tail = 0; } @@ -21,24 +17,16 @@ void queue::wait_and_push(T value) int spin = 0; bool spun_success = false; bool done = false; - while (true) - { - if (next_tail == head_cache) - { + while (true) { + if (next_tail == head_cache) { done = true; head_cache = head.load(std::memory_order_acquire); - if (next_tail == head_cache) - { - if (spin < spin_threshold) - { + if (next_tail == head_cache) { + if (spin < spin_threshold) { // Busy-wait - } - else if (spin < spin_threshold + 100) - { + } else if (spin < spin_threshold + 100) { std::this_thread::yield(); - } - else - { + } else { head.wait(head_cache, std::memory_order_acquire); } spin++; @@ -47,8 +35,7 @@ void queue::wait_and_push(T value) } // Refresh head_cache before was_empty calculation to ensure correctness - if (!done) - { + if (!done) { head_cache = head.load(std::memory_order_acquire); } spun_success = (spin < spin_threshold); @@ -57,8 +44,7 @@ void queue::wait_and_push(T value) tail_cache = next_tail; tail.store(tail_cache, std::memory_order_release); - if (was_empty) - { + if (was_empty) { tail.notify_one(); } break; @@ -66,27 +52,21 @@ void queue::wait_and_push(T value) // Adapt spin threshold for next time int delta = std::max(1, spin / 10); - if (spun_success) - { + if (spun_success) { spin_threshold = std::min(spin_threshold + delta, max_spin); - } - else - { + } else { spin_threshold = std::max(spin_threshold - delta, min_spin); } } template -bool queue::try_push(T value) -{ +bool lockfree_spsc_bounded::try_push(T value) { return emplace_back(std::move(value)); } template -bool queue::try_pop(T &value) -{ - if (tail_cache == head_cache) - { +bool lockfree_spsc_bounded::try_pop(T &value) { + if (tail_cache == head_cache) { tail_cache = tail.load(std::memory_order_acquire); // refresh cache if (tail_cache == head_cache) // empty { @@ -97,39 +77,29 @@ bool queue::try_pop(T &value) value = arr[head_cache]; head_cache = (head_cache + 1) % capacity; head.store(head_cache, std::memory_order_release); - if (was_full) - { + if (was_full) { head.notify_one(); } return true; } template -void queue::wait_and_pop(T &value) -{ +void lockfree_spsc_bounded::wait_and_pop(T &value) { static thread_local int spin_threshold = 100; const int min_spin = 10, max_spin = 1000; int spin = 0; bool spun_success = false; bool done = false; - while (true) - { - if (head_cache == tail_cache) - { + while (true) { + if (head_cache == tail_cache) { done = true; tail_cache = tail.load(std::memory_order_acquire); - if (head_cache == tail_cache) - { - if (spin < spin_threshold) - { + if (head_cache == tail_cache) { + if (spin < spin_threshold) { // Busy-wait - } - else if (spin < spin_threshold + 100) - { + } else if (spin < spin_threshold + 100) { std::this_thread::yield(); - } - else - { + } else { tail.wait(tail_cache, std::memory_order_acquire); } ++spin; @@ -138,26 +108,22 @@ void queue::wait_and_pop(T &value) } // Refresh tail_cache before was_full calculation to ensure correctness - if (!done) - { + if (!done) { tail_cache = tail.load(std::memory_order_acquire); } spun_success = (spin < spin_threshold); size_t next_tail = tail_cache + 1; - if (next_tail == capacity) - { + if (next_tail == capacity) { next_tail = 0; } bool was_full = (next_tail == head_cache); value = arr[head_cache]; head_cache = head_cache + 1; - if (head_cache == capacity) - { + if (head_cache == capacity) { head_cache = 0; } head.store(head_cache, std::memory_order_release); - if (was_full) - { + if (was_full) { head.notify_one(); } break; @@ -165,21 +131,16 @@ void queue::wait_and_pop(T &value) // Adapt spin threshold for next time int delta = std::max(1, spin / 10); - if (spun_success) - { + if (spun_success) { spin_threshold = std::min(spin_threshold + delta, max_spin); - } - else - { + } else { spin_threshold = std::max(spin_threshold - delta, min_spin); } } template -bool queue::peek(T &value) -{ - if (head_cache == tail_cache) - { +bool lockfree_spsc_bounded::peek(T &value) { + if (head_cache == tail_cache) { tail_cache = tail.load(std::memory_order_acquire); if (tail_cache == head_cache) // empty return false; @@ -189,17 +150,15 @@ bool queue::peek(T &value) } template -bool queue::empty() -{ - return head.load(std::memory_order_acquire) == tail.load(std::memory_order_acquire); +bool lockfree_spsc_bounded::empty() { + return head.load(std::memory_order_acquire) == + tail.load(std::memory_order_acquire); } template template -bool queue::emplace_back(Args &&...args) -{ - if ((tail_cache + 1) % capacity == head_cache) - { +bool lockfree_spsc_bounded::emplace_back(Args &&...args) { + if ((tail_cache + 1) % capacity == head_cache) { head_cache = head.load(std::memory_order_acquire); // refresh cache if ((tail_cache + 1) % capacity == head_cache) // full { @@ -210,20 +169,19 @@ bool queue::emplace_back(Args &&...args) arr[tail_cache] = T(std::forward(args)...); tail_cache = (tail_cache + 1) % capacity; tail.store(tail_cache, std::memory_order_release); - if (was_empty) - { + if (was_empty) { tail.notify_one(); } return true; } template -size_t queue::size() -{ +size_t lockfree_spsc_bounded::size() { size_t t = tail.load(std::memory_order_acquire); size_t h = head.load(std::memory_order_acquire); return (t - h + capacity) % capacity; } +} // namespace tsfqueue::impl #endif diff --git a/include/lockfree_spsc_unbounded/defs.hpp b/include/lockfree_spsc_unbounded/defs.hpp index c5df674..0dd5e7f 100644 --- a/include/lockfree_spsc_unbounded/defs.hpp +++ b/include/lockfree_spsc_unbounded/defs.hpp @@ -8,7 +8,7 @@ #include #include -namespace tsfqueue::__impl { +namespace tsfqueue::impl { template class lockfree_spsc_unbounded { // Works exactly same as the blocking_mpmc_unbounded queue (see this once) // with tail pointer pointing to stub node and your head pointer updates as @@ -48,14 +48,14 @@ template class lockfree_spsc_unbounded { // node. We handle the empty queue gracefully as per the pop type. private: - using node = tsfqueue::__utils::Lockless_Node; + using node = tsfqueue::utils::Lockless_Node; // Add the private members : // 1. node* head; // 2. node* tail; - alignas(tsfq::__impl::cache_line_size) node *head; - alignas(tsfq::__impl::cache_line_size) node *tail; + alignas(tsfq::impl::cache_line_size) node *head; + alignas(tsfq::impl::cache_line_size) node *tail; // Description of private members : // 1. node* head -> Pointer to the head node @@ -63,7 +63,7 @@ template class lockfree_spsc_unbounded { // 3. Cache align 1-2 // capacity - alignas(tsfq::__impl::cache_line_size) std::atomic capacity{0}; + alignas(tsfq::impl::cache_line_size) std::atomic capacity{0}; public: // Public member functions : @@ -137,6 +137,6 @@ template class lockfree_spsc_unbounded { // 10. Why no shared_ptr ?? [Reason this] // }; -} // namespace tsfqueue::__impl +} // namespace tsfqueue::impl #endif \ No newline at end of file diff --git a/include/lockfree_spsc_unbounded/impl.hpp b/include/lockfree_spsc_unbounded/impl.hpp index 8a1b219..f69b144 100644 --- a/include/lockfree_spsc_unbounded/impl.hpp +++ b/include/lockfree_spsc_unbounded/impl.hpp @@ -3,10 +3,8 @@ #include "defs.hpp" -template -using queue = tsfqueue::__impl::lockfree_spsc_unbounded; - -template void queue::push(T value) { +namespace tsfqueue::impl { +template void lockfree_spsc_unbounded::push(T value) { static_assert(std::is_move_constructible_v, "T must be move constructible"); @@ -14,7 +12,7 @@ template void queue::push(T value) { emplace_back(std::move(value)); } -template bool queue::try_pop(T &value) { +template bool lockfree_spsc_unbounded::try_pop(T &value) { static_assert(std::is_nothrow_destructible_v, "T must be nothrow destructible"); @@ -37,7 +35,7 @@ template bool queue::try_pop(T &value) { return true; } -template void queue::wait_and_pop(T &value) { +template void lockfree_spsc_unbounded::wait_and_pop(T &value) { static_assert(std::is_nothrow_destructible_v, "T must be nothrow destructible"); @@ -57,7 +55,7 @@ template void queue::wait_and_pop(T &value) { capacity.fetch_sub(1, std::memory_order_relaxed); } -template bool queue::unsafe_peek(T &value) { +template bool lockfree_spsc_unbounded::unsafe_peek(T &value) { static_assert(std::is_copy_assignable_v, "T must be copy-assignable"); @@ -69,13 +67,13 @@ template bool queue::unsafe_peek(T &value) { return true; } -template bool queue::empty(void) { +template bool lockfree_spsc_unbounded::empty(void) { return (head->next.load(std::memory_order_acquire) == nullptr); } template template -void queue::emplace_back(Args &&...args) { +void lockfree_spsc_unbounded::emplace_back(Args &&...args) { static_assert(std::is_constructible_v, "T must be constructible with Args&&..."); static_assert(std::is_default_constructible_v, @@ -92,9 +90,10 @@ void queue::emplace_back(Args &&...args) { tail = stub; } -template size_t queue::size() { +template size_t lockfree_spsc_unbounded::size() { return capacity.load(std::memory_order_relaxed); } +} // namespace tsfqueue::impl #endif diff --git a/include/utils.hpp b/include/utils.hpp index 469541f..b66b680 100644 --- a/include/utils.hpp +++ b/include/utils.hpp @@ -1,7 +1,16 @@ - #include - #include +#include +#include -namespace tsfqueue::__utils { +namespace tsfqueue::impl { +#ifdef __cpp_lib_hardware_interference_size +static constexpr size_t cache_line_size = + std::hardware_destructive_interference_size; +#else +static constexpr size_t cache_line_size = 64; // fallback +#endif +} // namespace tsfqueue::impl + +namespace tsfqueue::utils { template struct Node { std::shared_ptr data; std::unique_ptr> next; @@ -10,9 +19,4 @@ template struct Lockless_Node { T data; std::atomic next; }; -} // namespace tsfqueue::__utils - - namespace tsfq::__impl { - static constexpr size_t cache_line_size = - std::hardware_destructive_interference_size; - } \ No newline at end of file +} // namespace tsfqueue::utils \ No newline at end of file From a32ca47b3893f4b42fa3f3c72a0dc8751541b048 Mon Sep 17 00:00:00 2001 From: ToshitJain Date: Thu, 2 Apr 2026 12:43:01 +0530 Subject: [PATCH 24/30] Minor Debugging --- include/blocking_mpmc_unbounded/impl.hpp | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/include/blocking_mpmc_unbounded/impl.hpp b/include/blocking_mpmc_unbounded/impl.hpp index 54c464d..69156ff 100644 --- a/include/blocking_mpmc_unbounded/impl.hpp +++ b/include/blocking_mpmc_unbounded/impl.hpp @@ -3,12 +3,10 @@ #include "defs.hpp" -template using node = tsfqueue::utils::Node; - namespace tsfqueue::impl { template void blocking_mpmc_unbounded::push(T value) { - static_assert(std::is_copy_constructible_ || + static_assert(std::is_copy_constructible_v || std::is_move_constructible_v, "T must be copyable or movable to be pushed into the queue."); @@ -46,7 +44,7 @@ template void blocking_mpmc_unbounded::push(T value) { } template -std::unique_ptr blocking_mpmc_unbounded::wait_and_get() { +std::unique_ptr::node> blocking_mpmc_unbounded::wait_and_get() { // Locking the head mutex std::unique_lock head_lock(head_mutex); @@ -67,7 +65,7 @@ std::unique_ptr blocking_mpmc_unbounded::wait_and_get() { } template -std::unique_ptr blocking_mpmc_unbounded::try_get() { +std::unique_ptr::node> blocking_mpmc_unbounded::try_get() { std::lock_guard guard_head_mutex(head_mutex); if (size() > 0) { std::unique_ptr removing_node = std::move(head); @@ -218,7 +216,7 @@ std::shared_ptr blocking_mpmc_unbounded::unsafe_peek() { } template -std::unique_ptr blocking_mpmc_unbounded::wait_for_and_get( +std::unique_ptr::node> blocking_mpmc_unbounded::wait_for_and_get( std::chrono::milliseconds timeout) { // Using unique_lock to lock and unlock on our will. std::unique_lock lock_head(head_mutex); From cd05dd5a61256ded64fc50c316937d38e1b00f36 Mon Sep 17 00:00:00 2001 From: ToshitJain Date: Thu, 2 Apr 2026 15:15:43 +0530 Subject: [PATCH 25/30] Added Test Pipeline --- .github/workflows/tests.yaml | 100 +++++++++++++++++++++++ CMakeLists.txt | 25 ++++++ include/lockfree_spsc_bounded/defs.hpp | 12 +-- include/lockfree_spsc_unbounded/defs.hpp | 6 +- include/tsfqueue.hpp | 10 +++ include/utils.hpp | 11 ++- tests/test_mpmc.cpp | 25 ++++++ tests/test_mpsc.cpp | 0 tests/test_spsc.cpp | 44 ++++++++++ 9 files changed, 221 insertions(+), 12 deletions(-) create mode 100644 .github/workflows/tests.yaml create mode 100644 CMakeLists.txt create mode 100644 tests/test_mpmc.cpp create mode 100644 tests/test_mpsc.cpp create mode 100644 tests/test_spsc.cpp diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml new file mode 100644 index 0000000..2214848 --- /dev/null +++ b/.github/workflows/tests.yaml @@ -0,0 +1,100 @@ +name: Queue Tests + +on: + push: + pull_request: + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - name: Checkout repo + uses: actions/checkout@v3 + with: + fetch-depth: 0 + + # 🔹 Detect changed files + - name: Detect changes + id: changes + run: | + if [ "${{ github.event_name }}" = "pull_request" ]; then + BASE="origin/main" + git fetch origin main + else + BASE="${{ github.event.before }}" + fi + + echo "Base: $BASE" + echo "Head: ${{ github.sha }}" + + CHANGED_FILES=$(git diff --name-only $BASE ${{ github.sha }}) + + echo "Changed files:" + echo "$CHANGED_FILES" + + # SPSC (bounded + unbounded) + if echo "$CHANGED_FILES" | grep -E "lockfree_spsc"; then + echo "run_spsc=true" >> $GITHUB_OUTPUT + fi + + # MPSC + if echo "$CHANGED_FILES" | grep -E "lockfree_mpsc"; then + echo "run_mpsc=true" >> $GITHUB_OUTPUT + fi + + # MPMC + if echo "$CHANGED_FILES" | grep -E "lockfree_mpmc|blocking_mpmc"; then + echo "run_mpmc=true" >> $GITHUB_OUTPUT + fi + + # If tests themselves change → run corresponding + if echo "$CHANGED_FILES" | grep -E "test_spsc.cpp"; then + echo "run_spsc=true" >> $GITHUB_OUTPUT + fi + + if echo "$CHANGED_FILES" | grep -E "test_mpsc.cpp"; then + echo "run_mpsc=true" >> $GITHUB_OUTPUT + fi + + if echo "$CHANGED_FILES" | grep -E "test_mpmc.cpp"; then + echo "run_mpmc=true" >> $GITHUB_OUTPUT + fi + + # 🔹 Install deps + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y cmake g++ libgtest-dev + + # 🔹 Build GTest + - name: Build GTest + run: | + cd /usr/src/gtest + sudo cmake . + sudo make + sudo cp lib/*.a /usr/lib + + # 🔹 Build project + - name: Build project + run: | + rm -rf build + cmake -B build + cmake --build build + + # 🔹 Selective tests + - name: Run SPSC tests + if: steps.changes.outputs.run_spsc == 'true' + run: cd build && ./test_spsc + + - name: Run MPSC tests + if: steps.changes.outputs.run_mpsc == 'true' + run: cd build && ./test_mpsc + + - name: Run MPMC tests + if: steps.changes.outputs.run_mpmc == 'true' + run: cd build && ./test_mpmc + + # 🔹 Safety net (always runs) + # - name: Run full test suite (safety) + # run: cd build && ctest --output-on-failure \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..958fb15 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,25 @@ +cmake_minimum_required(VERSION 3.14) +project(ThreadsafeQueueLib) + +set(CMAKE_CXX_STANDARD 17) +enable_testing() + +find_package(GTest REQUIRED) +include_directories(include) + +# SPSC tests +add_executable(test_spsc tests/test_spsc.cpp) +target_link_libraries(test_spsc GTest::GTest GTest::Main pthread) + +# MPSC tests +add_executable(test_mpsc tests/test_mpsc.cpp) +target_link_libraries(test_mpsc GTest::GTest GTest::Main pthread) + +# MPMC tests +add_executable(test_mpmc tests/test_mpmc.cpp) +target_link_libraries(test_mpmc GTest::GTest GTest::Main pthread) + +# Register tests +add_test(NAME SPSC COMMAND test_spsc) +add_test(NAME MPSC COMMAND test_mpsc) +add_test(NAME MPMC COMMAND test_mpmc) \ No newline at end of file diff --git a/include/lockfree_spsc_bounded/defs.hpp b/include/lockfree_spsc_bounded/defs.hpp index c2b3695..e99be59 100644 --- a/include/lockfree_spsc_bounded/defs.hpp +++ b/include/lockfree_spsc_bounded/defs.hpp @@ -19,12 +19,12 @@ template class lockfree_spsc_bounded { // among the preferred endpoints as per use case. private: // Add the private members : - alignas(tsfq::__impl::cache_line_size) std::atomic head; - alignas(tsfq::__impl::cache_line_size) size_t tail_cache; - alignas(tsfq::__impl::cache_line_size) std::atomic tail; - alignas(tsfq::__impl::cache_line_size) size_t head_cache; + alignas(cache_line_size) std::atomic head; + alignas(cache_line_size) size_t tail_cache; + alignas(cache_line_size) std::atomic tail; + alignas(cache_line_size) size_t head_cache; static constexpr size_t capacity = Capacity + 1; - alignas(tsfq::__impl::cache_line_size) T arr[capacity]; + alignas(cache_line_size) T arr[capacity]; // aligned the start of the array too // Description of private members : // 1. std::atomic head is the atomic head pointer @@ -52,7 +52,7 @@ template class lockfree_spsc_bounded { static_assert(std::atomic::is_always_lock_free, "must be lock-free"); - static_assert(alignof(std::atomic) <= tsfq::__impl::cache_line_size, + static_assert(alignof(std::atomic) <= cache_line_size, "cache line size is inefficient"); public: diff --git a/include/lockfree_spsc_unbounded/defs.hpp b/include/lockfree_spsc_unbounded/defs.hpp index 0dd5e7f..047fb87 100644 --- a/include/lockfree_spsc_unbounded/defs.hpp +++ b/include/lockfree_spsc_unbounded/defs.hpp @@ -54,8 +54,8 @@ template class lockfree_spsc_unbounded { // 1. node* head; // 2. node* tail; - alignas(tsfq::impl::cache_line_size) node *head; - alignas(tsfq::impl::cache_line_size) node *tail; + alignas(cache_line_size) node *head; + alignas(cache_line_size) node *tail; // Description of private members : // 1. node* head -> Pointer to the head node @@ -63,7 +63,7 @@ template class lockfree_spsc_unbounded { // 3. Cache align 1-2 // capacity - alignas(tsfq::impl::cache_line_size) std::atomic capacity{0}; + alignas(cache_line_size) std::atomic capacity{0}; public: // Public member functions : diff --git a/include/tsfqueue.hpp b/include/tsfqueue.hpp index f90f2da..8f1f172 100644 --- a/include/tsfqueue.hpp +++ b/include/tsfqueue.hpp @@ -12,4 +12,14 @@ #include #endif +namespace tsfqueue +{ +template +using BlockingMPMCUnbounded = impl::blocking_mpmc_unbounded; +template +using SPSCBounded = impl::lockfree_spsc_bounded; +template +using SPSCUnbounded = impl::lockfree_spsc_unbounded; +} // namespace tsfqueue + #endif diff --git a/include/utils.hpp b/include/utils.hpp index b66b680..c016ba2 100644 --- a/include/utils.hpp +++ b/include/utils.hpp @@ -1,12 +1,15 @@ +#ifndef UTILS +#define UTILS + #include #include namespace tsfqueue::impl { #ifdef __cpp_lib_hardware_interference_size -static constexpr size_t cache_line_size = +inline constexpr size_t cache_line_size = std::hardware_destructive_interference_size; #else -static constexpr size_t cache_line_size = 64; // fallback +inline constexpr size_t cache_line_size = 64; // fallback #endif } // namespace tsfqueue::impl @@ -19,4 +22,6 @@ template struct Lockless_Node { T data; std::atomic next; }; -} // namespace tsfqueue::utils \ No newline at end of file +} // namespace tsfqueue::utils + +#endif \ No newline at end of file diff --git a/tests/test_mpmc.cpp b/tests/test_mpmc.cpp new file mode 100644 index 0000000..bce0ccb --- /dev/null +++ b/tests/test_mpmc.cpp @@ -0,0 +1,25 @@ +#include +#include +#include +#include + +#include "tsfqueue.hpp" + +// Basic sanity +TEST(MPMCQueue, BasicPushPop_Unbounded) { + tsfqueue::BlockingMPMCUnbounded q; + + EXPECT_TRUE(q.empty()); + + q.push(1); + q.push(2); + + int x; + EXPECT_TRUE(q.try_pop(x)); + EXPECT_EQ(x, 1); + + EXPECT_TRUE(q.try_pop(x)); + EXPECT_EQ(x, 2); + + EXPECT_TRUE(q.empty()); +} \ No newline at end of file diff --git a/tests/test_mpsc.cpp b/tests/test_mpsc.cpp new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_spsc.cpp b/tests/test_spsc.cpp new file mode 100644 index 0000000..c46cd4c --- /dev/null +++ b/tests/test_spsc.cpp @@ -0,0 +1,44 @@ +#include +#include +#include +#include + +#include "tsfqueue.hpp" + +// Basic sanity +TEST(SPSCQueue, BasicPushPop_Bounded) { + tsfqueue::SPSCBounded q; + + EXPECT_TRUE(q.empty()); + + EXPECT_TRUE(q.try_push(1)); + EXPECT_TRUE(q.try_push(2)); + + int x; + EXPECT_TRUE(q.try_pop(x)); + EXPECT_EQ(x, 1); + + EXPECT_TRUE(q.try_pop(x)); + EXPECT_EQ(x, 2); + + EXPECT_TRUE(q.empty()); +} + +// Basic sanity +TEST(SPSCQueue, BasicPushPop_Unbounded) { + tsfqueue::SPSCUnbounded q; + + EXPECT_TRUE(q.empty()); + + q.push(1); + q.push(2); + + int x; + EXPECT_TRUE(q.try_pop(x)); + EXPECT_EQ(x, 1); + + EXPECT_TRUE(q.try_pop(x)); + EXPECT_EQ(x, 2); + + EXPECT_TRUE(q.empty()); +} \ No newline at end of file From e7c12c5097d905fd5244ce46c2c9efaffeee7897 Mon Sep 17 00:00:00 2001 From: ToshitJain Date: Thu, 2 Apr 2026 16:51:23 +0530 Subject: [PATCH 26/30] Mac Sema/Slot + Block partial --- .../FAST_lockfree_spsc_unbounded/block.hpp | 61 ++++++++++ .../FAST_lockfree_spsc_unbounded/slots.hpp | 106 ++++++++++++++++++ 2 files changed, 167 insertions(+) create mode 100644 include/FAST_lockfree_spsc_unbounded/slots.hpp diff --git a/include/FAST_lockfree_spsc_unbounded/block.hpp b/include/FAST_lockfree_spsc_unbounded/block.hpp index e69de29..6c14a69 100644 --- a/include/FAST_lockfree_spsc_unbounded/block.hpp +++ b/include/FAST_lockfree_spsc_unbounded/block.hpp @@ -0,0 +1,61 @@ +#ifndef FAST_LOCKFREE_SPSC_UNBOUNDED_BLOCK +#define FAST_LOCKFREE_SPSC_UNBOUNDED_BLOCK + +#include "../utils.hpp" +#include "slots.hpp" +#include + +namespace tsfqueue::impl{ + template + class Block_FAST{ + private: + using Slot_FAST = tsfqueue::FAST::Slot_FAST; + std::size_t capacity; + std::size_t mask; + char* raw; + char* data; + std::unique_ptr slots; + template + void inner_enqueue(U&& data){ + + } + public: + Block_FAST(std::size_t cap){ + // Initialize by bringing it to the closest power of 2 + // And then make mask + slots(static_cast(cap)); + } + ~Block_FAST(){ + // Make destructor + } + Block_FAST(const Block_FAST& other) = delete; + Block_FAST& operator=(const Block_FAST& other) = delete; + Block_FAST(Block_FAST&& other){ + // Move constructor + } + template + bool try_enqueue(U&& data){ + // Try to check if can push + if(slots->try_get()){ + inner_enqueue(std::forward(data)); + return true; + } + return false; + } + template + void wait_enqueue(U&& data){ + slots->wait_and_get(); + inner_enqueue(std::forward(data)); + } + template + bool wait_enqueue_timed(U&& data, std::int64_t time_usecs){ + if(slots->timed_get(time_usecs)){ + inner_enqueue(std::forward(data)); + return true; + } + return false; + } + }; +} + +#endif \ No newline at end of file diff --git a/include/FAST_lockfree_spsc_unbounded/slots.hpp b/include/FAST_lockfree_spsc_unbounded/slots.hpp new file mode 100644 index 0000000..fb7ed72 --- /dev/null +++ b/include/FAST_lockfree_spsc_unbounded/slots.hpp @@ -0,0 +1,106 @@ +#ifndef FAST_LOCKFREE_SPSC_UNBOUNDED_SLOT +#define FAST_LOCKFREE_SPSC_UNBOUNDED_SLOT + +#include "../utils.hpp" +#include + +namespace tsfqueue::FAST{ +#if defined(__MACH__) +#include + class Semaphore_FAST{ + private: + semaphore_t sema; + Semaphore_FAST(const Semaphore& other) = delete; + Semaphore_FAST(Semaphore&& other) = delete; + public: + Semaphore_FAST(int count = 0){ + assert(count >= 0); + kern_return_t ret = semaphore_create(mach_task_self(), &sema, SYNC_POLICY_FIFO, count); + assert(ret == KERN_SUCCESS); + } + ~Semaphore_FAST(){ + semaphore_destroy(mach_task_self(), sema); + } + bool try_get(){ + return timed_get(0); + } + bool timed_get(std::uint64_t time_usecs){ + mach_timespec_t time; + time.tv_sec = static_cast(time_usecs / 1000000); + time.tv_nsec = static_cast((time_usecs % 1000000) * 1000); + kern_return_t ret = semaphore_timedwait(sema, time); + return ret == KERN_SUCCESS; + } + void wait_and_get(){ + semaphore_wait(sema); + } + void signal(int times = 1){ + while(times--) + while(semaphore_signal(sema) != KERN_SUCCESS); + } + }; +#endif + + + class Slot_FAST{ + private: + std::atomic counter; + Semaphore_FAST sema; + static constexpr int SPIN_COUNT = 1024; // Change this for benchmakrs & set to best possible + inline bool hot_path(){ + for (int i = 0; i < SPIN_COUNT; i++){ + if(counter.load(std::memory_order_acquire) > 0){ + counter.fetch_sub(1); + return true; + } + } + return false; + } + bool get_with_sleep(std::int64_t time_usecs = -1){ + if(hot_path()) return true; + counter.fetch_sub(1); + if(time_usecs < 0){ + sema.wait_and_get() + return true; + } + if(sema.timed_get(static_cast(time_usecs))){ + return true; + } + + // Restore the semaphore + // Signal happened just after timeout expired + // Thus we add a while loop which keeps checking until all boundary conditions + // are gone. Super clever. + while(true){ + int old = counter.fetch_add(1); + if(old < 0) return false; // Restored successfully + old = counter.fetch_sub(1); + if(old > 0 && sema.try_get()){ + return true; + } + } + } + public: + Slot_FAST(int count) : counter(count), sema(0) {} + bool try_get(){ + // Ok since SPSC so counter cannot be decremented between load and fetch_sub + if(counter.load(std::memory_order_acquire) > 0){ + counter.fetch_sub(1); + return true; + } + return false; + } + bool timed_get(std::int64_t time_usecs){ + return get_with_sleep(time_usecs); + } + void wait_and_get(){ + return get_with_sleep(); + } + void signal(int times = 1){ + int old = counter.fetch_add(1); + if(old < 0) sema.signal(); + } + } +}; + +#endif \ No newline at end of file From a39b4fa195f5daf81f4049273566aeb85203b1c4 Mon Sep 17 00:00:00 2001 From: Ritesh Raj Singh Date: Fri, 3 Apr 2026 18:08:23 +0530 Subject: [PATCH 27/30] simplified implementation --- include/lockfree_spsc_bounded/defs.hpp | 4 +- include/lockfree_spsc_bounded/impl.hpp | 239 +++++++++---------------- 2 files changed, 82 insertions(+), 161 deletions(-) diff --git a/include/lockfree_spsc_bounded/defs.hpp b/include/lockfree_spsc_bounded/defs.hpp index e99be59..e864a4a 100644 --- a/include/lockfree_spsc_bounded/defs.hpp +++ b/include/lockfree_spsc_bounded/defs.hpp @@ -71,11 +71,11 @@ template class lockfree_spsc_bounded { // 4. bool try_pop(value ref) : Try to pop and return false if failed bool bool try_pop(T &); // 5. empty(void) : Checks if the queue is empty and return bool - bool empty(); + bool empty() const; // 6. bool peek(value ref) : Peek the top of the queue. bool peek(T &); template bool emplace_back(Args &&...args); - size_t size(); + size_t size() const; // Will work only in SPSC/MPSC why ?? [Reason this] // 8. Add emplace_back using perfect forwarding and variadic templates (you // can use this in push then) diff --git a/include/lockfree_spsc_bounded/impl.hpp b/include/lockfree_spsc_bounded/impl.hpp index ebc32a7..4ee2c92 100644 --- a/include/lockfree_spsc_bounded/impl.hpp +++ b/include/lockfree_spsc_bounded/impl.hpp @@ -4,189 +4,110 @@ #include "defs.hpp" #include -namespace tsfqueue::impl { -template -void lockfree_spsc_bounded::wait_and_push(T value) { - size_t next_tail = tail_cache + 1; - if (next_tail == capacity) { - next_tail = 0; - } - - static thread_local int spin_threshold = 100; - const int min_spin = 10, max_spin = 1000; - int spin = 0; - bool spun_success = false; - bool done = false; - while (true) { - if (next_tail == head_cache) { - done = true; - head_cache = head.load(std::memory_order_acquire); - if (next_tail == head_cache) { - if (spin < spin_threshold) { - // Busy-wait - } else if (spin < spin_threshold + 100) { - std::this_thread::yield(); - } else { - head.wait(head_cache, std::memory_order_acquire); - } - spin++; - continue; - } +namespace tsfqueue::impl +{ + template + void lockfree_spsc_bounded::wait_and_push(T value) + { + size_t next_tail = (tail_cache + 1) % capacity; + + while (next_tail == head_cache) + { + head_cache = head.load(std::memory_order_acquire); // busy wait } - // Refresh head_cache before was_empty calculation to ensure correctness - if (!done) { - head_cache = head.load(std::memory_order_acquire); - } - spun_success = (spin < spin_threshold); - bool was_empty = (head_cache == tail_cache); arr[tail_cache] = std::move(value); tail_cache = next_tail; tail.store(tail_cache, std::memory_order_release); - - if (was_empty) { - tail.notify_one(); - } - break; } - // Adapt spin threshold for next time - int delta = std::max(1, spin / 10); - if (spun_success) { - spin_threshold = std::min(spin_threshold + delta, max_spin); - } else { - spin_threshold = std::max(spin_threshold - delta, min_spin); + template + bool lockfree_spsc_bounded::try_push(T value) + { + return emplace_back(std::move(value)); } -} -template -bool lockfree_spsc_bounded::try_push(T value) { - return emplace_back(std::move(value)); -} - -template -bool lockfree_spsc_bounded::try_pop(T &value) { - if (tail_cache == head_cache) { - tail_cache = tail.load(std::memory_order_acquire); // refresh cache - if (tail_cache == head_cache) // empty + template + bool lockfree_spsc_bounded::try_pop(T &value) + { + if (tail_cache == head_cache) { - return false; - } - } - bool was_full = (tail_cache + 1) % capacity == head_cache; - value = arr[head_cache]; - head_cache = (head_cache + 1) % capacity; - head.store(head_cache, std::memory_order_release); - if (was_full) { - head.notify_one(); - } - return true; -} - -template -void lockfree_spsc_bounded::wait_and_pop(T &value) { - static thread_local int spin_threshold = 100; - const int min_spin = 10, max_spin = 1000; - int spin = 0; - bool spun_success = false; - bool done = false; - while (true) { - if (head_cache == tail_cache) { - done = true; tail_cache = tail.load(std::memory_order_acquire); - if (head_cache == tail_cache) { - if (spin < spin_threshold) { - // Busy-wait - } else if (spin < spin_threshold + 100) { - std::this_thread::yield(); - } else { - tail.wait(tail_cache, std::memory_order_acquire); - } - ++spin; - continue; + if (tail_cache == head_cache) + { + return false; } } - // Refresh tail_cache before was_full calculation to ensure correctness - if (!done) { - tail_cache = tail.load(std::memory_order_acquire); - } - spun_success = (spin < spin_threshold); - size_t next_tail = tail_cache + 1; - if (next_tail == capacity) { - next_tail = 0; - } - bool was_full = (next_tail == head_cache); - value = arr[head_cache]; - head_cache = head_cache + 1; - if (head_cache == capacity) { - head_cache = 0; - } + value = std::move(arr[head_cache]); + head_cache = (head_cache + 1) % capacity; head.store(head_cache, std::memory_order_release); - if (was_full) { - head.notify_one(); - } - break; + return true; } - // Adapt spin threshold for next time - int delta = std::max(1, spin / 10); - if (spun_success) { - spin_threshold = std::min(spin_threshold + delta, max_spin); - } else { - spin_threshold = std::max(spin_threshold - delta, min_spin); - } -} + template + void lockfree_spsc_bounded::wait_and_pop(T &value) + { + while (head_cache == tail_cache) + { + tail_cache = tail.load(std::memory_order_acquire); // busy wait + } -template -bool lockfree_spsc_bounded::peek(T &value) { - if (head_cache == tail_cache) { - tail_cache = tail.load(std::memory_order_acquire); - if (tail_cache == head_cache) // empty - return false; + value = std::move(arr[head_cache]); + head_cache = (head_cache + 1) % capacity; + head.store(head_cache, std::memory_order_release); } - value = arr[head_cache]; - return true; -} -template -bool lockfree_spsc_bounded::empty() { - return head.load(std::memory_order_acquire) == - tail.load(std::memory_order_acquire); -} + template + bool lockfree_spsc_bounded::peek(T &value) + { + if (head_cache == tail_cache) + { + tail_cache = tail.load(std::memory_order_acquire); + if(head_cache == tail_cache) + { + return false; + } + } + value = arr[head_cache]; + return true; + } -template -template -bool lockfree_spsc_bounded::emplace_back(Args &&...args) { - if ((tail_cache + 1) % capacity == head_cache) { - head_cache = head.load(std::memory_order_acquire); // refresh cache - if ((tail_cache + 1) % capacity == head_cache) // full + template + template + bool lockfree_spsc_bounded::emplace_back(Args &&...args) + { + if ((tail_cache + 1) % capacity == head_cache) { - return false; + head_cache = head.load(std::memory_order_acquire); + if ((tail_cache + 1) % capacity == head_cache) + { + return false; + } } + + arr[tail_cache] = T(std::forward(args)...); + tail_cache = (tail_cache + 1) % capacity; + tail.store(tail_cache, std::memory_order_release); + return true; } - bool was_empty = (head_cache == tail_cache); - arr[tail_cache] = T(std::forward(args)...); - tail_cache = (tail_cache + 1) % capacity; - tail.store(tail_cache, std::memory_order_release); - if (was_empty) { - tail.notify_one(); + + template + bool lockfree_spsc_bounded::empty() const + { + return head.load(std::memory_order_relaxed) == + tail.load(std::memory_order_relaxed); + // since queue is very frequently modified + } + + template + size_t lockfree_spsc_bounded::size() const + { + return (tail.load(std::memory_order_relaxed) - + head.load(std::memory_order_relaxed) + + capacity) % capacity; + // again, since size is very frequently changing. } - return true; -} - -template -size_t lockfree_spsc_bounded::size() { - size_t t = tail.load(std::memory_order_acquire); - size_t h = head.load(std::memory_order_acquire); - return (t - h + capacity) % capacity; -} } // namespace tsfqueue::impl -#endif - -// 1. Add static asserts -// 2. Add emplace_back using perfect forwarding and variadic templates (you -// can use this in push then) -// 3. Add size() function -// 4. Any more suggestions ?? \ No newline at end of file +#endif \ No newline at end of file From 5f663f3de44eeb6a4fde5867b42b99795cb8618f Mon Sep 17 00:00:00 2001 From: Ritesh Raj Singh Date: Fri, 3 Apr 2026 22:59:40 +0530 Subject: [PATCH 28/30] updated head and tail cache --- include/lockfree_spsc_bounded/impl.hpp | 88 ++++++++++++++------------ 1 file changed, 47 insertions(+), 41 deletions(-) diff --git a/include/lockfree_spsc_bounded/impl.hpp b/include/lockfree_spsc_bounded/impl.hpp index 4ee2c92..03b2dd1 100644 --- a/include/lockfree_spsc_bounded/impl.hpp +++ b/include/lockfree_spsc_bounded/impl.hpp @@ -9,16 +9,17 @@ namespace tsfqueue::impl template void lockfree_spsc_bounded::wait_and_push(T value) { - size_t next_tail = (tail_cache + 1) % capacity; + size_t tail_load = tail.load(std::memory_order_relaxed); + size_t next_tail = (tail_load + 1) % capacity; while (next_tail == head_cache) { head_cache = head.load(std::memory_order_acquire); // busy wait } - arr[tail_cache] = std::move(value); + arr[tail_load] = std::move(value); tail_cache = next_tail; - tail.store(tail_cache, std::memory_order_release); + tail.store(next_tail, std::memory_order_release); } template @@ -27,86 +28,91 @@ namespace tsfqueue::impl return emplace_back(std::move(value)); } + template + template + bool lockfree_spsc_bounded::emplace_back(Args &&...args) + { + size_t tail_load = tail.load(std::memory_order_relaxed); + if ((tail_load + 1) % capacity == head_cache) + { + head_cache = head.load(std::memory_order_acquire); + if ((tail_load + 1) % capacity == head_cache) + { + return false; + } + } + + arr[tail_load] = T(std::forward(args)...); + tail_load = (tail_load + 1) % capacity; + tail.store(tail_load, std::memory_order_release); + return true; + } + template bool lockfree_spsc_bounded::try_pop(T &value) { - if (tail_cache == head_cache) + size_t head_load = head.load(std::memory_order_relaxed); + if (tail_cache == head_load) { tail_cache = tail.load(std::memory_order_acquire); - if (tail_cache == head_cache) + if (tail_cache == head_load) { return false; } } - value = std::move(arr[head_cache]); - head_cache = (head_cache + 1) % capacity; - head.store(head_cache, std::memory_order_release); + value = std::move(arr[head_load]); + head_load = (head_load + 1) % capacity; + head.store(head_load, std::memory_order_release); return true; } template void lockfree_spsc_bounded::wait_and_pop(T &value) { - while (head_cache == tail_cache) + size_t head_load = head.load(std::memory_order_relaxed); + while (head_load == tail_cache) { tail_cache = tail.load(std::memory_order_acquire); // busy wait } - value = std::move(arr[head_cache]); - head_cache = (head_cache + 1) % capacity; - head.store(head_cache, std::memory_order_release); + value = std::move(arr[head_load]); + head_load = (head_load + 1) % capacity; + head.store(head_load, std::memory_order_release); } template bool lockfree_spsc_bounded::peek(T &value) { - if (head_cache == tail_cache) + size_t head_load = head.load(std::memory_order_relaxed); + if (head_load == tail_cache) { tail_cache = tail.load(std::memory_order_acquire); - if(head_cache == tail_cache) + if (head_load == tail_cache) { return false; } } - value = arr[head_cache]; + value = arr[head_load]; return true; } - template - template - bool lockfree_spsc_bounded::emplace_back(Args &&...args) - { - if ((tail_cache + 1) % capacity == head_cache) - { - head_cache = head.load(std::memory_order_acquire); - if ((tail_cache + 1) % capacity == head_cache) - { - return false; - } - } - - arr[tail_cache] = T(std::forward(args)...); - tail_cache = (tail_cache + 1) % capacity; - tail.store(tail_cache, std::memory_order_release); - return true; - } - template bool lockfree_spsc_bounded::empty() const { - return head.load(std::memory_order_relaxed) == + return head.load(std::memory_order_relaxed) == tail.load(std::memory_order_relaxed); - // since queue is very frequently modified + // since queue is very frequently modified } - + template size_t lockfree_spsc_bounded::size() const { - return (tail.load(std::memory_order_relaxed) - - head.load(std::memory_order_relaxed) + - capacity) % capacity; - // again, since size is very frequently changing. + return (tail.load(std::memory_order_relaxed) - + head.load(std::memory_order_relaxed) + + capacity) % + capacity; + // again, since size is very frequently changing. } } // namespace tsfqueue::impl From c8dbbe10ff910de2c4548f176ffcc0ca57fb2ac5 Mon Sep 17 00:00:00 2001 From: Ritesh Raj Singh Date: Fri, 3 Apr 2026 23:36:04 +0530 Subject: [PATCH 29/30] updated spsc bounded --- include/lockfree_spsc_bounded/impl.hpp | 83 +++++++++++++------------- 1 file changed, 41 insertions(+), 42 deletions(-) diff --git a/include/lockfree_spsc_bounded/impl.hpp b/include/lockfree_spsc_bounded/impl.hpp index 03b2dd1..aa72edc 100644 --- a/include/lockfree_spsc_bounded/impl.hpp +++ b/include/lockfree_spsc_bounded/impl.hpp @@ -9,16 +9,16 @@ namespace tsfqueue::impl template void lockfree_spsc_bounded::wait_and_push(T value) { - size_t tail_load = tail.load(std::memory_order_relaxed); - size_t next_tail = (tail_load + 1) % capacity; - + size_t cur_tail = tail.load(std::memory_order_acquire); + size_t next_tail = (cur_tail + 1) % capacity; + // size_t curr_head = head.load(std::memory_order_acquire); while (next_tail == head_cache) { head_cache = head.load(std::memory_order_acquire); // busy wait } - arr[tail_load] = std::move(value); - tail_cache = next_tail; + arr[cur_tail] = std::move(value); + // tail_cache = next_tail; tail.store(next_tail, std::memory_order_release); } @@ -28,72 +28,71 @@ namespace tsfqueue::impl return emplace_back(std::move(value)); } - template - template - bool lockfree_spsc_bounded::emplace_back(Args &&...args) - { - size_t tail_load = tail.load(std::memory_order_relaxed); - if ((tail_load + 1) % capacity == head_cache) - { - head_cache = head.load(std::memory_order_acquire); - if ((tail_load + 1) % capacity == head_cache) - { - return false; - } - } - - arr[tail_load] = T(std::forward(args)...); - tail_load = (tail_load + 1) % capacity; - tail.store(tail_load, std::memory_order_release); - return true; - } - template bool lockfree_spsc_bounded::try_pop(T &value) { - size_t head_load = head.load(std::memory_order_relaxed); - if (tail_cache == head_load) + // cur_tail = tail.load(std::memory_order_acquire); + size_t cur_head = head.load(std::memory_order_acquire); + if (tail_cache == cur_head) { tail_cache = tail.load(std::memory_order_acquire); - if (tail_cache == head_load) - { + if (tail_cache == cur_head) return false; - } } - value = std::move(arr[head_load]); - head_load = (head_load + 1) % capacity; - head.store(head_load, std::memory_order_release); + value = std::move(arr[cur_head]); + // head_cache = (cur_head + 1) % capacity; + head.store((cur_head + 1) % capacity, std::memory_order_release); return true; } template void lockfree_spsc_bounded::wait_and_pop(T &value) { - size_t head_load = head.load(std::memory_order_relaxed); - while (head_load == tail_cache) + size_t cur_head = head.load(std::memory_order_acquire); + while (tail_cache == cur_head) { tail_cache = tail.load(std::memory_order_acquire); // busy wait } - value = std::move(arr[head_load]); - head_load = (head_load + 1) % capacity; - head.store(head_load, std::memory_order_release); + value = std::move(arr[cur_head]); + // head_cache = (cur_head + 1) % capacity; + head.store((cur_head + 1) % capacity, std::memory_order_release); } template bool lockfree_spsc_bounded::peek(T &value) { - size_t head_load = head.load(std::memory_order_relaxed); - if (head_load == tail_cache) + size_t cur_head = head.load(std::memory_order_acquire); + if (cur_head == tail_cache) { tail_cache = tail.load(std::memory_order_acquire); - if (head_load == tail_cache) + if (cur_head == tail_cache) { return false; } } - value = arr[head_load]; + value = arr[cur_head]; + return true; + } + + template + template + bool lockfree_spsc_bounded::emplace_back(Args &&...args) + { + size_t cur_tail = tail.load(std::memory_order_acquire); + size_t next_tail = (cur_tail + 1) % capacity; + if (next_tail == head_cache) + { + head_cache = head.load(std::memory_order_acquire); + if (next_tail == head_cache) + { + return false; + } + } + arr[cur_tail] = T(std::forward(args)...); + // tail_cache = next_tail; + tail.store(next_tail, std::memory_order_release); return true; } From 106e1b99d500ffe6bfbfc5c725d8d62e60df42a0 Mon Sep 17 00:00:00 2001 From: Aryan Gupta Date: Fri, 3 Apr 2026 23:48:30 +0530 Subject: [PATCH 30/30] Add tests for BlockingMPMCUnbounded functionality --- tests/test_mpmc.cpp | 195 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 193 insertions(+), 2 deletions(-) diff --git a/tests/test_mpmc.cpp b/tests/test_mpmc.cpp index bce0ccb..06e3422 100644 --- a/tests/test_mpmc.cpp +++ b/tests/test_mpmc.cpp @@ -1,9 +1,12 @@ #include +#include #include #include #include - +#include #include "tsfqueue.hpp" +#include + // Basic sanity TEST(MPMCQueue, BasicPushPop_Unbounded) { @@ -22,4 +25,192 @@ TEST(MPMCQueue, BasicPushPop_Unbounded) { EXPECT_EQ(x, 2); EXPECT_TRUE(q.empty()); -} \ No newline at end of file +} + +TEST(MPMCQueue, WaitFor_isTimeExact) { + tsfqueue::BlockingMPMCUnbounded q; + + EXPECT_TRUE(q.empty()); + + q.push(1); + q.push(2); + + int x; + EXPECT_TRUE(q.try_pop(x)); + EXPECT_EQ(x, 1); + + EXPECT_TRUE(q.try_pop(x)); + EXPECT_EQ(x, 2); + + EXPECT_TRUE(q.empty()); + + int wtime = 200; + int error = 10; + // auto func = [&](){ + // std::this_thread::sleep_for(std::chrono::seconds(10)); + // q.push(12); + // q.push(31); + // }; + // std::thread t(func); + auto start = std::chrono::high_resolution_clock::now(); + int res = q.wait_for_and_pop(x, std::chrono::milliseconds(wtime)); + auto end = std::chrono::high_resolution_clock::now(); + auto duration = std::chrono::duration_cast(end - start).count(); + EXPECT_NEAR((int)duration, (int)wtime, error); +} + +TEST(MPMCQueue, WaitForTester) { + tsfqueue::BlockingMPMCUnbounded q; + + EXPECT_TRUE(q.empty()); + + q.push(1); + q.push(2); + + int x; + EXPECT_TRUE(q.try_pop(x)); + EXPECT_EQ(x, 1); + + EXPECT_TRUE(q.try_pop(x)); + EXPECT_EQ(x, 2); + + EXPECT_TRUE(q.empty()); + + int wtime = 4000; + int pushTime = 2000; + int error = 50; + auto func = [&](){ + std::this_thread::sleep_for(std::chrono::milliseconds(pushTime)); + q.push(12); + }; + std::thread t(func); + auto start = std::chrono::high_resolution_clock::now(); + EXPECT_TRUE(q.wait_for_and_pop(x, std::chrono::milliseconds(wtime))); + auto end = std::chrono::high_resolution_clock::now(); + auto duration = std::chrono::duration_cast(end - start).count(); + EXPECT_NEAR((int)duration, (int)pushTime, error); + + EXPECT_EQ(x, 12); + + EXPECT_TRUE(q.empty()); + t.join(); +} + +TEST(MPMCQueue, DataRaceStressTest) { + tsfqueue::BlockingMPMCUnbounded q; + const int num_threads = 8; + const int ops_per_thread = 1000; + std::atomic total_popped{0}; + std::vector producers; + std::vector consumers; + + // Launch Producers + for (int i = 0; i < num_threads; ++i) { + producers.emplace_back([&q, ops_per_thread]() { + for (int j = 0; j < ops_per_thread; ++j) { + q.push(j); + } + }); + } + + // Launch Consumers + for (int i = 0; i < num_threads; ++i) { + consumers.emplace_back([&q, ops_per_thread, &total_popped]() { + for (int j = 0; j < ops_per_thread; ++j) { + int val; + // Using wait_for to ensure we don't block forever if a push is missed + if (q.wait_for_and_pop(val, std::chrono::milliseconds(100))) { + total_popped++; + } + } + }); + } + + for (auto& t : producers) t.join(); + for (auto& t : consumers) t.join(); + + // Verify all items were accounted for + EXPECT_EQ(total_popped.load(), num_threads * ops_per_thread); + EXPECT_TRUE(q.empty()); +} + +// Testing Emplace Back working and Static Assert working, i.e it does not create a copy of the object. +struct MockObject { + static int copies; + static int moves; + int x; + MockObject(int val) : x(val) {} + MockObject(const MockObject& other) { copies++; } + MockObject(MockObject&& other) noexcept { moves++; } + MockObject& operator=(const MockObject& other) { + x = other.x; + copies++; + return *this; + } + + MockObject& operator=(MockObject&& other) noexcept { + x = other.x; + moves++; + return *this; + } +}; +int MockObject::copies = 0; +int MockObject::moves = 0; + +TEST(MPMCQueue, EmplaceBackEfficiency_and_StaticAsserts) { + tsfqueue::BlockingMPMCUnbounded q; + MockObject::copies = 0; + MockObject::moves = 0; + // Emplace should construct the object directly in the internal storage + q.emplace_back(42); + + EXPECT_EQ(MockObject::copies, 0); + // Depending on implementation, moves should ideally be 0 or 1 + EXPECT_LE(MockObject::moves, 1); + + MockObject out(0); + + EXPECT_TRUE(q.try_pop(out)); + EXPECT_EQ(out.x, 42); +} + +// --------------------------------------------------------- +// Helper: A type that is NEITHER copyable NOR movable +// --------------------------------------------------------- +struct LockedType { + LockedType() = default; + LockedType(const LockedType&) = delete; + LockedType(LockedType&&) = delete; + LockedType& operator=(const LockedType&) = delete; + LockedType& operator=(LockedType&&) = delete; +}; + +// --------------------------------------------------------- +// Helper: A type that is ONLY movable +// --------------------------------------------------------- +struct MoveOnly { + MoveOnly() = default; + MoveOnly(const MoveOnly&) = delete; + MoveOnly(MoveOnly&&) = default; + MoveOnly& operator=(const MoveOnly&) = delete; + MoveOnly& operator=(MoveOnly&&) = default; +}; + +// --------------------------------------------------------- +// The Test Case +// --------------------------------------------------------- +TEST(MPMCQueue, StaticRequirementsValidation) { + // 1. Test Copy/Move Constructibility (Required for Push/Emplace) + bool is_pushable = std::is_copy_constructible_v || std::is_move_constructible_v; + EXPECT_TRUE(is_pushable) << "Queue must support copy or move for push operations."; + + // 2. Test Assignability (Required for try_pop(T& value)) + // This mirrors your failing static_assert: + // static_assert(std::is_copy_assignable_v || std::is_move_assignable_v) + + bool move_only_assignable = std::is_copy_assignable_v || std::is_move_assignable_v; + EXPECT_TRUE(move_only_assignable) << "Move-only types should be allowed if they are move-assignable."; + + bool locked_type_assignable = std::is_copy_assignable_v || std::is_move_assignable_v; + EXPECT_FALSE(locked_type_assignable) << "LockedType should fail the assignability requirement."; +}