├── .github └── workflows │ └── c-cpp.yml ├── .gitignore ├── Makefile ├── LICENSE ├── example ├── example_cpp11.cc ├── example1.cc └── example2.cc ├── README.md ├── concurrent_queue.h └── test └── test.cc /.github/workflows/c-cpp.yml: -------------------------------------------------------------------------------- 1 | name: C/C++ CI 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v4 16 | - name: make 17 | run: make 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Prerequisites 2 | *.d 3 | 4 | # Compiled Object files 5 | *.slo 6 | *.lo 7 | *.o 8 | *.obj 9 | 10 | # Precompiled Headers 11 | *.gch 12 | *.pch 13 | 14 | # Compiled Dynamic libraries 15 | *.so 16 | *.dylib 17 | *.dll 18 | 19 | # Fortran module files 20 | *.mod 21 | *.smod 22 | 23 | # Compiled Static libraries 24 | *.lai 25 | *.la 26 | *.a 27 | *.lib 28 | 29 | # Executables 30 | *.exe 31 | *.out 32 | *.app 33 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | BIN_PATH:=./bin 2 | 3 | COMPILER:=c++ 4 | 5 | run_test : test example1 example2 example_cpp11 6 | $(BIN_PATH)/test 7 | $(BIN_PATH)/example1 8 | $(BIN_PATH)/example2 9 | $(BIN_PATH)/example_cpp11 10 | 11 | test : $(BIN_PATH)/test 12 | 13 | example1 : $(BIN_PATH)/example1 14 | 15 | example2 : $(BIN_PATH)/example2 16 | 17 | example_cpp11 : $(BIN_PATH)/example_cpp11 18 | 19 | $(BIN_PATH)/test : test/test.cc concurrent_queue.h third_party/catch.hpp | build_prepare 20 | $(COMPILER) $< -o $@ 21 | 22 | $(BIN_PATH)/example1 : example/example1.cc concurrent_queue.h | build_prepare 23 | $(COMPILER) $< -o $@ 24 | 25 | $(BIN_PATH)/example2 : example/example2.cc concurrent_queue.h | build_prepare 26 | $(COMPILER) $< -o $@ 27 | 28 | $(BIN_PATH)/example_cpp11 : example/example_cpp11.cc concurrent_queue.h | build_prepare 29 | $(COMPILER) $< -o $@ -std=c++11 30 | 31 | build_prepare: 32 | @mkdir -p $(BIN_PATH) 33 | 34 | clean: 35 | @rm -f $(BIN_PATH)/test $(BIN_PATH)/example1 $(BIN_PATH)/example2 $(BIN_PATH)/example_cpp11 36 | @if [ -d "$(BIN_PATH)" ] && [ -z "$$(ls -A $(BIN_PATH))" ]; then \ 37 | rmdir $(BIN_PATH); \ 38 | fi -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Hanwen Zheng 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /example/example_cpp11.cc: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Hanwen Zheng 3 | * @email eserinc.z@outlook.com 4 | * @create date 2024-05-08 15:22:09 5 | * @modify date 2024-05-08 15:22:09 6 | * @desc Simple code testing basic function in C++11. This source file is 7 | * supposed to be compile with '-std=c++11'. 8 | */ 9 | #include 10 | 11 | #include "../concurrent_queue.h" 12 | 13 | #define ASSERT(x) \ 14 | do { \ 15 | if (!(x)) { \ 16 | std::cerr << __FILE__ << ":" << __LINE__ << " " << #x << " failed" \ 17 | << std::endl; \ 18 | abort(); \ 19 | } \ 20 | } while (false) 21 | 22 | void TestUnlimited() { 23 | fox_cq::ConcurrentQueue q; 24 | 25 | ASSERT(q.UnlimitedSize()); 26 | ASSERT(!q.LimitedSize()); 27 | 28 | q.Push(1); 29 | q.Push(2); 30 | q.Push(3); 31 | q.SetFinish(); 32 | ASSERT(q.Size() == 3); 33 | int value; 34 | ASSERT(q.Pop(value)); 35 | ASSERT(value == 1); 36 | ASSERT(q.Size() == 2); 37 | 38 | ASSERT(q.Pop(value)); 39 | ASSERT(value == 2); 40 | ASSERT(q.Size() == 1); 41 | 42 | ASSERT(q.Pop(value)); 43 | ASSERT(value == 3); 44 | ASSERT(q.Size() == 0); 45 | 46 | ASSERT(!q.Pop(value)); 47 | } 48 | 49 | void TestLimited() { 50 | fox_cq::ConcurrentQueue q; 51 | 52 | ASSERT(!q.UnlimitedSize()); 53 | ASSERT(q.LimitedSize()); 54 | 55 | q.Push(1); 56 | q.Push(2); 57 | q.Push(3); 58 | q.SetFinish(); 59 | ASSERT(q.Size() == 3); 60 | int value; 61 | ASSERT(q.Pop(value)); 62 | ASSERT(value == 1); 63 | ASSERT(q.Size() == 2); 64 | 65 | ASSERT(q.Pop(value)); 66 | ASSERT(value == 2); 67 | ASSERT(q.Size() == 1); 68 | 69 | ASSERT(q.Pop(value)); 70 | ASSERT(value == 3); 71 | ASSERT(q.Size() == 0); 72 | 73 | ASSERT(!q.Pop(value)); 74 | } 75 | 76 | int main() { 77 | std::cout << " -----Example_cpp11 begin----" << std::endl; 78 | TestUnlimited(); 79 | TestLimited(); 80 | std::cout << "Passed in C++11" << std::endl; 81 | std::cout << " -----Example_cpp11 end----" << std::endl; 82 | 83 | return 0; 84 | } -------------------------------------------------------------------------------- /example/example1.cc: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Hanwen Zheng 3 | * @email eserinc.z@outlook.com 4 | * @create date 2024-05-08 09:12:50 5 | * @modify date 2024-05-08 09:12:50 6 | * @desc A simple example showing the concurrent queue how to use. 7 | */ 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | #include "../concurrent_queue.h" 17 | 18 | const int RangePerProducer = 10000; 19 | using CQueue = fox_cq::ConcurrentQueue>; 20 | std::atomic ncompleted{0}; 21 | 22 | void Producer(int id, int total_num, CQueue& queue) { 23 | for (int i = 0; i < RangePerProducer; i++) { 24 | queue.Push(std::make_unique(i + id * RangePerProducer)); 25 | } 26 | if (++ncompleted == total_num) { 27 | // last producer that compeleted producing should set the queue to finish 28 | // since no more production to push into queue. 29 | queue.SetFinish(); 30 | } 31 | } 32 | 33 | void Consumer(int id, std::vector>& collections, 34 | CQueue& queue) { 35 | while (true) { 36 | std::unique_ptr result; 37 | if (!queue.Pop(result)) { 38 | // return if no more production. 39 | return; 40 | } 41 | collections[id].push_back(*result); 42 | } 43 | } 44 | 45 | int main(){ 46 | std::cout << " -----Example1 begin----" << std::endl; 47 | // multiple producer and multiple consumer are allowed 48 | CQueue q; 49 | const int NProducer = 5; 50 | const int NConsumer = 5; 51 | std::vector> collections(NConsumer); 52 | std::vector threads; 53 | for (int i = 0; i < NProducer; i++) { 54 | threads.emplace_back(&Producer, i, NProducer, std::ref(q)); 55 | } 56 | for(int i=0;i< NConsumer;i++){ 57 | threads.emplace_back(&Consumer, i, std::ref(collections), std::ref(q)); 58 | } 59 | 60 | for(auto& thread : threads){ 61 | thread.join(); 62 | } 63 | 64 | std::vector in; 65 | for(int i=0;i out; 70 | for (int i = 0; i < NConsumer; i++) { 71 | out.insert(out.end(), collections[i].begin(), collections[i].end()); 72 | } 73 | std::sort(out.begin(),out.end()); 74 | 75 | bool suc = (in == out); 76 | 77 | std::cout << "Consumers can catch all the production from producer? " 78 | << (suc ? "Yes" : "No") << std::endl; 79 | if (suc) { 80 | std::cout << "Passed" << std::endl; 81 | } else { 82 | std::cout << "Failed" << std::endl; 83 | } 84 | std::cout << " -----Example1 end----" << std::endl; 85 | 86 | return suc ? 0 : 1; 87 | } 88 | -------------------------------------------------------------------------------- /example/example2.cc: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Hanwen Zheng 3 | * @email eserinc.z@outlook.com 4 | * @create date 2024-05-08 09:13:32 5 | * @modify date 2024-05-08 09:13:32 6 | * @desc Another simple example showing that object lifetime is handled well. 7 | */ 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | #include "../concurrent_queue.h" 17 | 18 | const int RangePerProducer = 10000; 19 | 20 | struct MemoryLeakTestStruct { 21 | static std::atomic remaining_count; 22 | static std::atomic constructed_count; 23 | MemoryLeakTestStruct() : controller(std::make_unique()) { 24 | ++constructed_count; 25 | } 26 | MemoryLeakTestStruct(MemoryLeakTestStruct&&) = default; 27 | MemoryLeakTestStruct& operator=(MemoryLeakTestStruct&&) = default; 28 | struct CntControl { 29 | CntControl() { ++remaining_count; } 30 | ~CntControl() { --remaining_count; } 31 | }; 32 | std::unique_ptr controller; 33 | }; 34 | 35 | std::atomic MemoryLeakTestStruct::remaining_count; 36 | std::atomic MemoryLeakTestStruct::constructed_count; 37 | 38 | using CQueue = fox_cq::ConcurrentQueue; 39 | std::atomic ncompleted{0}; 40 | 41 | void Producer(int id, int total_num, CQueue& queue) { 42 | for (int i = 0; i < RangePerProducer; i++) { 43 | queue.Push(MemoryLeakTestStruct{}); 44 | } 45 | if (++ncompleted == total_num) { 46 | // last producer that compeleted producing should set the queue to finish 47 | // since no more production to push into queue. 48 | queue.SetFinish(); 49 | } 50 | } 51 | 52 | void Consumer(int id, CQueue& queue) { 53 | while (true) { 54 | if (!queue.Pop()) { 55 | // return if no more production. 56 | return; 57 | } 58 | } 59 | } 60 | 61 | int main(){ 62 | std::cout << " -----Example2 begin----" << std::endl; 63 | // multiple producer and multiple consumer are allowed 64 | CQueue q; 65 | const int NProducer = 5; 66 | const int NConsumer = 5; 67 | MemoryLeakTestStruct::remaining_count = 0; 68 | MemoryLeakTestStruct::constructed_count = 0; 69 | 70 | std::vector threads; 71 | for (int i = 0; i < NProducer; i++) { 72 | threads.emplace_back(&Producer, i, NProducer, std::ref(q)); 73 | } 74 | for(int i=0;i< NConsumer;i++){ 75 | threads.emplace_back(&Consumer, i, std::ref(q)); 76 | } 77 | 78 | for(auto& thread : threads){ 79 | thread.join(); 80 | } 81 | 82 | std::cout << "Total constructed number of object: " 83 | << MemoryLeakTestStruct::constructed_count << "\n" 84 | << "Remaining number of object after consuming: " 85 | << MemoryLeakTestStruct::remaining_count << std::endl; 86 | if (MemoryLeakTestStruct::remaining_count == 0) { 87 | std::cout << "Passed" << std::endl; 88 | } else { 89 | std::cout << "Failed" << std::endl; 90 | } 91 | std::cout << " -----Example2 end----" << std::endl; 92 | 93 | return MemoryLeakTestStruct::remaining_count == 0 ? 0 : 1; 94 | } 95 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Simple Concurrent Queue 2 | A simple header only concurrent queue using std::mutex and std::condition_variable in C++11. 3 | 4 | This concurrent queue utilizes locks (std::mutex) and sleep mechanisms (std::condition_variable). 5 | The lock protects internal data, and if the queue is full during `Push` or empty during `Pop`, it uses sleep to wait. It decouples the circular queue and other internal data structures from concurrency control logic, making the logic simple and easy to understand. 6 | 7 | Its design primarily targets blocking requirements; that is, its `Pop` operation will wait for another thread to `Push` in an element or mark the queue as having no subsequent elements. 8 | However, it also provides `TryPop` non-blocking operations, which immediately return the current result without waiting. 9 | 10 | ## Use and not to Use 11 | This queue is suitable for requirements where data exchange frequency is not very high; its automatic sleep during blocking significantly reduces CPU consumption from busy waiting. For high-frequency data exchange requirements, it is recommended to use lock-free queues. 12 | 13 | 14 | 15 | # Setup 16 | Just put the only header `concurrent_queue.h` in your project and include it. 17 | 18 | 19 | # Usage 20 | 21 | ## Declaration 22 | Limited size queue (circular buffer) 23 | ``` 24 | /* 25 | * q1 will reserve space for eight elements. 26 | * When attempting to push more than 8 elements into it, 27 | * the thread will wait until there is a consumer 28 | * to make space in the queue. 29 | */ 30 | ConcurrentQueue q1; 31 | ``` 32 | Unlimited size queue 33 | ``` 34 | /* 35 | * q2 will allocate memory when needed. 36 | * There won't be any waiting during element pushing. 37 | */ 38 | ConcurrentQueue q2; 39 | ``` 40 | Notice that void type are supported. 41 | The main difference from normal types is that you cannot specify instances 42 | during Push or Pop; it can only act as a counter and serves as a semaphore. 43 | ``` 44 | ConcurrentQueue semaphore; 45 | ``` 46 | 47 | ## Operation 48 | They support the following operations regardless of the type. 49 | ### Push 50 | ``` 51 | // Push a default constructed new item into back of the queue 52 | void ConcurrentQueue::Push() 53 | 54 | // Move and push `item` into back of the queue 55 | // Enabled when T != void 56 | void ConcurrentQueue::Push(T&& item) 57 | 58 | // Copy and push `item` into back of of the queue 59 | // Enabled when T != void 60 | void ConcurrentQueue::Push(const T& item) 61 | ``` 62 | ### Pop 63 | ``` 64 | // Pop out and discard the front element, will wait for element to push. (blocking, may wait other thread to push new element) 65 | // Return true on success. 66 | // Return false on failure (trying to 67 | // pop from a finished and empty queue). 68 | bool ConcurrentQueue::Pop() 69 | 70 | // Pop out the front element to `result`, will wait for element to push. (blocking, may wait other thread to push new element) 71 | // Return true on success. 72 | // Return false on failure (trying to 73 | // pop from a finished and empty queue). 74 | // Enabled when T != void 75 | bool ConcurrentQueue::Pop(T& result) 76 | ``` 77 | ### TryPop 78 | ``` 79 | // Pop out and discard the front element. (non-blocking, return immediately) 80 | // Return true on success. 81 | // Return false on failure (trying to 82 | // pop from an empty queue). 83 | bool ConcurrentQueue::TryPop() 84 | 85 | // Pop out the front element to `result`. (non-blocking, return immediately) 86 | // Return true on success. 87 | // Return false on failure (trying to 88 | // pop from an empty queue). 89 | // Enabled when T != void 90 | bool ConcurrentQueue::TryPop(T& result) 91 | ``` 92 | ### Others 93 | ``` 94 | // Return number of element in the queue 95 | std::size_t Size() const 96 | 97 | // Also, copy and move are supported. 98 | ``` 99 | 100 | ## Example 101 | 102 | ``` 103 | ConcurrentQueue q; 104 | ``` 105 | Only one producer and only consumer for simplicity. 106 | Multiple producer and consumer are supported. 107 | 108 | ### Thread producer 109 | ``` 110 | q.Push("7"); 111 | q.Push("123"); 112 | q.Push("456"); 113 | // No more input. 114 | // If not set finish, consumer's third `Pop` will wait producer's another `Push` forever. 115 | q.SetFinish(); 116 | ``` 117 | ### Thread consumer 118 | ``` 119 | bool suc; 120 | std::string s; 121 | 122 | while(!q.TryPop(s)) { 123 | // busy waiting 124 | }; 125 | assert(s == "7"); 126 | 127 | suc = q.Pop(s); 128 | assert(suc); 129 | assert(s == "123"); 130 | 131 | suc = q.Pop(s); 132 | assert(suc); 133 | assert(s == "456"); 134 | 135 | suc = q.Pop(s); 136 | assert(!suc); 137 | ``` 138 | 139 | For more details, please refer to `example/example1.cc` and `example/example2.cc`. 140 | 141 | # Benchmarks 142 | 143 | Performance tests can be referenced at this link [moodycamel](https://moodycamel.com/blog/2014/a-fast-general-purpose-lock-free-queue-for-c++.htm#benchmarks), where std::queue + std::mutex and the non-blocking part of this concurrent queue (i.e., the `TryPop` interface) share the same implementation principles and exhibit similar performance. Under high-frequency data exchange, its performance is inferior to lock-free implementations. This concurrent queue should primarily be used for blocking requirements, providing a simple and usable implementation for such needs. 144 | 145 | 146 | 147 | -------------------------------------------------------------------------------- /concurrent_queue.h: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Hanwen Zheng 3 | * @email eserinc.z@outlook.com 4 | * @create date 2024-05-08 09:10:15 5 | * @modify date 2024-05-08 09:10:15 6 | * @desc A simple concurrent queue supporting multiple producer and multiple 7 | * consumer using std::mutex and std::condition_variable. 8 | */ 9 | #pragma once 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | namespace fox_cq { 17 | 18 | static const std::size_t ConcurrentQueueUnlimitedSize = 19 | static_cast(-1); 20 | 21 | namespace internal { 22 | 23 | template 24 | class ConcurrentQueueContainer { 25 | public: 26 | ConcurrentQueueContainer() : head_(0), tail_(0), size_(0) { 27 | data_.reserve(MaxSize); 28 | } 29 | ConcurrentQueueContainer(const ConcurrentQueueContainer&) = default; 30 | ConcurrentQueueContainer(ConcurrentQueueContainer&&) = default; 31 | ConcurrentQueueContainer& operator=(const ConcurrentQueueContainer&) = 32 | default; 33 | ConcurrentQueueContainer& operator=(ConcurrentQueueContainer&&) = default; 34 | 35 | template 36 | void Push(U&& value) { 37 | assert(size_ < MaxSize); 38 | if (data_.size() <= tail_) [[unlikely]] { 39 | data_.emplace_back(std::forward(value)); 40 | } else { 41 | new (&data_[tail_]) T(std::forward(value)); 42 | } 43 | if (tail_ < MaxSize - 1) { 44 | ++tail_; 45 | } else { 46 | tail_ = 0; 47 | } 48 | ++size_; 49 | } 50 | 51 | void Push() { 52 | assert(size_ < MaxSize); 53 | if (data_.size() <= tail_) [[unlikely]] { 54 | data_.emplace_back(); 55 | } else { 56 | new (&data_[tail_]) T(); 57 | } 58 | if (tail_ < MaxSize - 1) { 59 | ++tail_; 60 | } else { 61 | tail_ = 0; 62 | } 63 | ++size_; 64 | } 65 | 66 | void Pop(T& value) { 67 | assert(size_ > 0); 68 | value = std::move(data_[head_]); 69 | data_[head_].~T(); 70 | if (head_ < MaxSize - 1) { 71 | ++head_; 72 | } else { 73 | head_ = 0; 74 | } 75 | --size_; 76 | } 77 | 78 | void Pop() { 79 | assert(size_ > 0); 80 | data_[head_].~T(); 81 | if (head_ < MaxSize - 1) { 82 | ++head_; 83 | } else { 84 | head_ = 0; 85 | } 86 | --size_; 87 | } 88 | 89 | std::size_t Size() const { return size_; } 90 | 91 | std::size_t Capacity() const { return MaxSize; } 92 | 93 | bool Empty() const { return size_ == 0; } 94 | 95 | bool Full() const { return size_ == MaxSize; } 96 | 97 | private: 98 | std::vector data_; 99 | std::size_t head_; 100 | std::size_t tail_; 101 | std::size_t size_; 102 | }; 103 | 104 | template 105 | class ConcurrentQueueContainer { 106 | public: 107 | ConcurrentQueueContainer() = default; 108 | ConcurrentQueueContainer(const ConcurrentQueueContainer&) = default; 109 | ConcurrentQueueContainer(ConcurrentQueueContainer&&) = default; 110 | ConcurrentQueueContainer& operator=(const ConcurrentQueueContainer&) = 111 | default; 112 | ConcurrentQueueContainer& operator=(ConcurrentQueueContainer&&) = default; 113 | 114 | template 115 | void Push(TValue&& value) { 116 | data_.push(std::forward(value)); 117 | } 118 | 119 | void Push() { data_.emplace(); } 120 | 121 | void Pop(T& value) { 122 | assert(!data_.empty()); 123 | value = std::move(data_.front()); 124 | data_.pop(); 125 | } 126 | 127 | void Pop() { 128 | assert(!data_.empty()); 129 | data_.pop(); 130 | } 131 | 132 | std::size_t Size() const { return data_.size(); } 133 | 134 | bool Empty() const { return data_.empty(); } 135 | 136 | bool Full() const { return false; } 137 | 138 | private: 139 | std::queue data_; 140 | }; 141 | 142 | template 143 | class ConcurrentQueueContainer { 144 | public: 145 | ConcurrentQueueContainer() : size_(0) {} 146 | ConcurrentQueueContainer(const ConcurrentQueueContainer&) = default; 147 | ConcurrentQueueContainer(ConcurrentQueueContainer&&) = default; 148 | ConcurrentQueueContainer& operator=(const ConcurrentQueueContainer&) = 149 | default; 150 | ConcurrentQueueContainer& operator=(ConcurrentQueueContainer&&) = default; 151 | 152 | void Push() { 153 | assert(size_ < MaxSize); 154 | ++size_; 155 | } 156 | 157 | void Pop() { 158 | assert(size_ > 0); 159 | --size_; 160 | } 161 | 162 | std::size_t Size() const { return size_; } 163 | 164 | std::size_t Capacity() const { return MaxSize; } 165 | 166 | bool Empty() const { return size_ == 0; } 167 | 168 | bool Full() const { return size_ == MaxSize; } 169 | 170 | private: 171 | std::size_t size_; 172 | }; 173 | 174 | template <> 175 | class ConcurrentQueueContainer { 176 | public: 177 | ConcurrentQueueContainer() : size_(0) {} 178 | ConcurrentQueueContainer(const ConcurrentQueueContainer&) = default; 179 | ConcurrentQueueContainer(ConcurrentQueueContainer&&) = default; 180 | ConcurrentQueueContainer& operator=(const ConcurrentQueueContainer&) = 181 | default; 182 | ConcurrentQueueContainer& operator=(ConcurrentQueueContainer&&) = default; 183 | 184 | void Push() { ++size_; } 185 | 186 | void Pop() { 187 | assert(size_ > 0); 188 | --size_; 189 | } 190 | 191 | std::size_t Size() const { return size_; } 192 | 193 | bool Empty() const { return size_ == 0; } 194 | 195 | bool Full() const { return false; } 196 | 197 | private: 198 | std::size_t size_; 199 | }; 200 | } // namespace internal 201 | 202 | template 203 | class ConcurrentQueue { 204 | public: 205 | ConcurrentQueue() = default; 206 | ConcurrentQueue(const ConcurrentQueue& other) { 207 | std::lock(lock_, other.lock_); 208 | std::lock_guard guard1(lock_, std::adopt_lock); 209 | std::lock_guard guard2(other.lock_, std::adopt_lock); 210 | data_ = other.data_; 211 | finished_ = other.finished_; 212 | WakeupAll(); 213 | other.WakeupAll(); 214 | }; 215 | ConcurrentQueue(ConcurrentQueue&& other) { 216 | std::lock(lock_, other.lock_); 217 | std::lock_guard guard1(lock_, std::adopt_lock); 218 | std::lock_guard guard2(other.lock_, std::adopt_lock); 219 | data_ = std::move(other.data_); 220 | finished_ = other.finished_; 221 | WakeupAll(); 222 | other.WakeupAll(); 223 | } 224 | ConcurrentQueue& operator=(const ConcurrentQueue& other) { 225 | if (this != &other) { 226 | std::lock(lock_, other.lock_); 227 | std::lock_guard guard1(lock_, std::adopt_lock); 228 | std::lock_guard guard2(other.lock_, std::adopt_lock); 229 | data_ = other.data_; 230 | finished_ = other.finished_; 231 | WakeupAll(); 232 | other.WakeupAll(); 233 | } 234 | return *this; 235 | } 236 | ConcurrentQueue& operator=(ConcurrentQueue&& other) { 237 | if (this != &other) { 238 | std::lock(lock_, other.lock_); 239 | std::lock_guard guard1(lock_, std::adopt_lock); 240 | std::lock_guard guard2(other.lock_, std::adopt_lock); 241 | data_ = std::move(other.data_); 242 | finished_ = other.finished_; 243 | WakeupAll(); 244 | other.WakeupAll(); 245 | } 246 | return *this; 247 | } 248 | 249 | ~ConcurrentQueue() { SetFinish(); } 250 | 251 | // Mark the queue has no more `Push` operation. 252 | // `Push` operation after `SetFinish` will be ignored. 253 | // Notice that `Pop` operation still works for remaining elements in the 254 | // queue. 255 | void SetFinish() { 256 | { 257 | std::lock_guard guard{lock_}; 258 | finished_ = true; 259 | } 260 | WakeupAll(); 261 | } 262 | 263 | // Push a default constructed new item into back of the queue 264 | void Push() { PushImpl(); } 265 | 266 | // Move and push `item` into back of the queue 267 | // Enabled when T != void 268 | template 269 | void Push( 270 | typename std::enable_if::value, U&&>::type item) { 271 | PushImpl(std::move(item)); 272 | } 273 | 274 | // Copy and push `item` into back of of the queue 275 | // Enabled when T != void 276 | template 277 | void Push(typename std::enable_if::value, 278 | const U&>::type item) { 279 | PushImpl(item); 280 | } 281 | 282 | // Pop out and discard the front element. (non-blocking, return immediately) 283 | // Return true on success. 284 | // Return false on failure (trying to 285 | // pop from an empty queue). 286 | bool TryPop() { return TryPopImpl(); } 287 | 288 | // Pop out the front element to `result`. (non-blocking, return immediately) 289 | // Return true on success. 290 | // Return false on failure (trying to 291 | // pop from an empty queue). 292 | // Enabled when T != void 293 | template 294 | bool TryPop( 295 | typename std::enable_if::value, U&>::type result) { 296 | return TryPopImpl(result); 297 | } 298 | 299 | // Pop out and discard the front element, will wait for element to push. (blocking, may wait other thread to push new element) 300 | // Return true on success. 301 | // Return false on failure (trying to 302 | // pop from a finished and empty queue). 303 | bool Pop() { return PopImpl(); } 304 | 305 | // Pop out the front element to `result`, will wait for element to push. (blocking, may wait other thread to push new element) 306 | // Return true on success. 307 | // Return false on failure (trying to 308 | // pop from a finished and empty queue). 309 | // Enabled when T != void 310 | template 311 | bool Pop( 312 | typename std::enable_if::value, U&>::type result) { 313 | return PopImpl(result); 314 | } 315 | 316 | // Return number of element in the queue 317 | std::size_t Size() const { 318 | std::lock_guard guard{lock_}; 319 | return data_.Size(); 320 | } 321 | 322 | // Return true iff this queue has no limit. 323 | bool UnlimitedSize() const { return MaxSize == ConcurrentQueueUnlimitedSize; } 324 | 325 | // Return true iff this queue has limit. 326 | bool LimitedSize() const { return MaxSize != ConcurrentQueueUnlimitedSize; } 327 | 328 | private: 329 | void WakeupAll() const { 330 | empty_cond_.notify_all(); 331 | // Full waiting only happens in limited size. 332 | if (LimitedSize()) full_cond_.notify_all(); 333 | } 334 | 335 | template 336 | void PushImpl(Args&&... item) { 337 | std::unique_lock lk{lock_}; 338 | if (LimitedSize() && data_.Full() && !finished_) { 339 | full_cond_.wait(lk, [this] { return !data_.Full() || finished_; }); 340 | } 341 | if (finished_) { 342 | // finished, should notify other threads to stop waiting. 343 | WakeupAll(); 344 | return; 345 | } 346 | data_.Push(std::forward(item)...); 347 | if (LimitedSize() && !data_.Full()) { 348 | full_cond_.notify_one(); 349 | } 350 | empty_cond_.notify_one(); 351 | } 352 | 353 | template 354 | bool PopImpl(Args&&... result) { 355 | std::unique_lock lk{lock_}; 356 | empty_cond_.wait(lk, [this] { return !data_.Empty() || finished_; }); 357 | if (!data_.Empty()) { 358 | data_.Pop(std::forward(result)...); 359 | if (!data_.Empty()) { 360 | empty_cond_.notify_one(); 361 | } else if (finished_) { 362 | // finished, should notify other threads to stop waiting. 363 | WakeupAll(); 364 | return true; 365 | } 366 | if (LimitedSize()) full_cond_.notify_one(); 367 | 368 | return true; 369 | } 370 | 371 | assert(finished_); 372 | // finished, should notify other threads to stop waiting. 373 | WakeupAll(); 374 | return false; 375 | } 376 | 377 | template 378 | bool TryPopImpl(Args&&... result) { 379 | std::unique_lock lk{lock_}; 380 | if (!data_.Empty()) { 381 | data_.Pop(std::forward(result)...); 382 | 383 | if (LimitedSize()) full_cond_.notify_one(); 384 | return true; 385 | } 386 | return false; 387 | } 388 | 389 | mutable std::mutex lock_; 390 | mutable std::condition_variable empty_cond_; 391 | mutable std::condition_variable full_cond_; 392 | internal::ConcurrentQueueContainer data_; 393 | bool finished_ = false; 394 | }; 395 | 396 | } // namespace fox_cq 397 | -------------------------------------------------------------------------------- /test/test.cc: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Hanwen Zheng 3 | * @email eserinc.z@outlook.com 4 | * @create date 2024-05-08 09:12:24 5 | * @modify date 2024-05-08 09:12:24 6 | * @desc Some tests for concurrent queue. 7 | */ 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | #include "../concurrent_queue.h" 17 | #define CATCH_CONFIG_MAIN 18 | #include "../third_party/catch.hpp" 19 | 20 | using namespace fox_cq; 21 | 22 | int destrct_cnt; 23 | 24 | struct MoveOnlyStruct { 25 | MoveOnlyStruct(int v) { 26 | if (v >= 0) { 27 | value = std::make_unique(v); 28 | } 29 | }; 30 | MoveOnlyStruct(const MoveOnlyStruct& other) = delete; 31 | MoveOnlyStruct(MoveOnlyStruct&& other) = default; 32 | MoveOnlyStruct& operator=(MoveOnlyStruct&& other) = default; 33 | ~MoveOnlyStruct() { 34 | if (value) { 35 | // std::cout << "MoveOnlyStruct#" << *value << " destructed.\n"; 36 | ++destrct_cnt; 37 | } 38 | } 39 | std::unique_ptr value; 40 | }; 41 | 42 | TEST_CASE("std::unique_ptr in limited sized concurrent queue", 43 | "") { 44 | using Unique = std::unique_ptr; 45 | ConcurrentQueue c; 46 | c.Push(std::make_unique(1)); 47 | Unique v; 48 | c.Pop(v); 49 | REQUIRE(v); 50 | REQUIRE(*v == 1); 51 | } 52 | 53 | TEST_CASE("std::unique_ptr in limited sized concurrent queue, non-blocking", 54 | "(non-blocking)") { 55 | using Unique = std::unique_ptr; 56 | ConcurrentQueue c; 57 | c.Push(std::make_unique(1)); 58 | Unique v; 59 | bool suc = c.TryPop(v); 60 | REQUIRE(suc); 61 | REQUIRE(v); 62 | REQUIRE(*v == 1); 63 | } 64 | 65 | TEST_CASE( 66 | "MoveOnlyStruct in limited sized concurrent queue destruct at the right " 67 | "time", 68 | "") { 69 | destrct_cnt = 0; 70 | ConcurrentQueue c; 71 | c.Push(MoveOnlyStruct(0)); 72 | REQUIRE(destrct_cnt == 0); 73 | { 74 | MoveOnlyStruct tmp(-1); 75 | c.Pop(tmp); 76 | } 77 | REQUIRE(destrct_cnt == 1); 78 | c.Push(MoveOnlyStruct(1)); 79 | c.Push(MoveOnlyStruct(2)); 80 | c.Push(MoveOnlyStruct(3)); 81 | c.Push(MoveOnlyStruct(4)); 82 | REQUIRE(destrct_cnt == 1); 83 | { 84 | MoveOnlyStruct tmp(-1); 85 | c.Pop(tmp); 86 | } 87 | REQUIRE(destrct_cnt == 2); 88 | { 89 | MoveOnlyStruct tmp(-1); 90 | c.Pop(tmp); 91 | } 92 | REQUIRE(destrct_cnt == 3); 93 | { 94 | MoveOnlyStruct tmp(-1); 95 | c.Pop(tmp); 96 | } 97 | REQUIRE(destrct_cnt == 4); 98 | { 99 | MoveOnlyStruct tmp(-1); 100 | c.Pop(tmp); 101 | } 102 | REQUIRE(destrct_cnt == 5); 103 | } 104 | 105 | TEST_CASE("std::unique_ptr in unlimited sized concurrent queue", 106 | "") { 107 | using Unique = std::unique_ptr; 108 | ConcurrentQueue c; 109 | c.Push(std::make_unique(1)); 110 | Unique v; 111 | c.Pop(v); 112 | assert(v && *v == 1); 113 | } 114 | 115 | TEST_CASE( 116 | "MoveOnlyStruct in unlimited sized concurrent queue destruct at the right " 117 | "time", 118 | "") { 119 | destrct_cnt = 0; 120 | ConcurrentQueue c; 121 | c.Push(MoveOnlyStruct(0)); 122 | REQUIRE(destrct_cnt == 0); 123 | { 124 | MoveOnlyStruct tmp(-1); 125 | c.Pop(tmp); 126 | } 127 | REQUIRE(destrct_cnt == 1); 128 | c.Push(MoveOnlyStruct(1)); 129 | c.Push(MoveOnlyStruct(2)); 130 | c.Push(MoveOnlyStruct(3)); 131 | c.Push(MoveOnlyStruct(4)); 132 | REQUIRE(destrct_cnt == 1); 133 | { 134 | MoveOnlyStruct tmp(-1); 135 | c.Pop(tmp); 136 | } 137 | REQUIRE(destrct_cnt == 2); 138 | { 139 | MoveOnlyStruct tmp(-1); 140 | c.Pop(tmp); 141 | } 142 | REQUIRE(destrct_cnt == 3); 143 | { 144 | MoveOnlyStruct tmp(-1); 145 | c.Pop(tmp); 146 | } 147 | REQUIRE(destrct_cnt == 4); 148 | { 149 | MoveOnlyStruct tmp(-1); 150 | c.Pop(tmp); 151 | } 152 | REQUIRE(destrct_cnt == 5); 153 | } 154 | 155 | TEST_CASE("void typed limited sized concurrent queue", "") { 156 | ConcurrentQueue q; 157 | q.Push(); 158 | REQUIRE(q.Pop() == 1); 159 | q.Push(); 160 | q.Push(); 161 | q.Push(); 162 | q.Push(); 163 | REQUIRE(q.Pop() == 1); 164 | REQUIRE(q.Pop() == 1); 165 | REQUIRE(q.Pop() == 1); 166 | REQUIRE(q.Pop() == 1); 167 | } 168 | 169 | TEST_CASE("void typed unlimited sized concurrent queue", 170 | "") { 171 | ConcurrentQueue q; 172 | q.Push(); 173 | assert(q.Pop() == 1); 174 | q.Push(); 175 | q.Push(); 176 | q.Push(); 177 | q.Push(); 178 | REQUIRE(q.Pop() == 1); 179 | REQUIRE(q.Pop() == 1); 180 | REQUIRE(q.Pop() == 1); 181 | REQUIRE(q.Pop() == 1); 182 | } 183 | 184 | TEST_CASE("Basic parallel test for limited size concurrent queue", 185 | "[Parallel]") { 186 | const int size = 1000; 187 | srand(time(nullptr)); 188 | std::vector buf; 189 | for (int i = 0; i < size; i++) { 190 | buf.push_back(i); 191 | } 192 | 193 | std::random_device rd; 194 | std::mt19937 g(rd()); 195 | std::shuffle(buf.begin(), buf.end(), g); 196 | 197 | ConcurrentQueue q; 198 | const int nthreadput = 3; 199 | const int nthreadget = 2; 200 | 201 | std::vector threads; 202 | 203 | std::atomic completed_put{0}; 204 | for (int i = 0; i < nthreadput; i++) { 205 | int l = i * size / nthreadput; 206 | int r = (i + 1) * size / nthreadput; 207 | threads.emplace_back( 208 | [&](int l, int r) { 209 | for (int i = l; i < r; i++) { 210 | q.Push(buf[i]); 211 | } 212 | if (++completed_put == nthreadput) { 213 | q.SetFinish(); 214 | } 215 | }, 216 | l, r); 217 | } 218 | std::vector> collections(nthreadget); 219 | for (int i = 0; i < nthreadget; i++) { 220 | threads.emplace_back( 221 | [&](int id) { 222 | int x; 223 | while (true) { 224 | bool suc = q.Pop(x); 225 | if (suc) { 226 | collections[id].push_back(x); 227 | } else { 228 | break; 229 | } 230 | } 231 | }, 232 | i); 233 | } 234 | for (auto& thread : threads) { 235 | thread.join(); 236 | } 237 | std::multiset in(buf.begin(), buf.end()); 238 | std::multiset out; 239 | for (int i = 0; i < nthreadget; i++) { 240 | out.insert(collections[i].begin(), collections[i].end()); 241 | } 242 | REQUIRE(in == out); 243 | } 244 | 245 | TEST_CASE("Basic parallel test for unlimited size concurrent queue", 246 | "[Parallel]") { 247 | const int size = 1000; 248 | srand(time(nullptr)); 249 | std::vector buf; 250 | for (int i = 0; i < size; i++) { 251 | buf.push_back(i); 252 | } 253 | 254 | std::random_device rd; 255 | std::mt19937 g(rd()); 256 | std::shuffle(buf.begin(), buf.end(), g); 257 | 258 | ConcurrentQueue q; 259 | const int nthreadput = 3; 260 | const int nthreadget = 2; 261 | 262 | std::vector threads; 263 | 264 | std::atomic completed_put{0}; 265 | for (int i = 0; i < nthreadput; i++) { 266 | int l = i * size / nthreadput; 267 | int r = (i + 1) * size / nthreadput; 268 | threads.emplace_back( 269 | [&](int l, int r) { 270 | for (int i = l; i < r; i++) { 271 | q.Push(buf[i]); 272 | } 273 | if (++completed_put == nthreadput) { 274 | q.SetFinish(); 275 | } 276 | }, 277 | l, r); 278 | } 279 | std::vector> collections(nthreadget); 280 | for (int i = 0; i < nthreadget; i++) { 281 | threads.emplace_back( 282 | [&](int id) { 283 | int x; 284 | while (true) { 285 | bool suc = q.Pop(x); 286 | if (suc) { 287 | collections[id].push_back(x); 288 | } else { 289 | break; 290 | } 291 | } 292 | }, 293 | i); 294 | } 295 | for (auto& thread : threads) { 296 | thread.join(); 297 | } 298 | std::multiset in(buf.begin(), buf.end()); 299 | std::multiset out; 300 | for (int i = 0; i < nthreadget; i++) { 301 | out.insert(collections[i].begin(), collections[i].end()); 302 | } 303 | REQUIRE(in == out); 304 | } 305 | 306 | TEST_CASE("Basic parallel test for void typed unlimited size concurrent queue", 307 | "[Parallel]") { 308 | const int size = 1000; 309 | srand(time(nullptr)); 310 | 311 | ConcurrentQueue q; 312 | const int nthreadput = 3; 313 | const int nthreadget = 2; 314 | 315 | std::vector threads; 316 | 317 | std::atomic completed_put{0}; 318 | for (int i = 0; i < nthreadput; i++) { 319 | int l = i * size / nthreadput; 320 | int r = (i + 1) * size / nthreadput; 321 | threads.emplace_back( 322 | [&](int l, int r) { 323 | for (int i = l; i < r; i++) { 324 | q.Push(); 325 | } 326 | if (++completed_put == nthreadput) { 327 | q.SetFinish(); 328 | } 329 | }, 330 | l, r); 331 | } 332 | std::vector collections(nthreadget); 333 | for (int i = 0; i < nthreadget; i++) { 334 | threads.emplace_back( 335 | [&](int id) { 336 | while (true) { 337 | auto suc = q.Pop(); 338 | if (suc) { 339 | ++collections[id]; 340 | } else { 341 | break; 342 | } 343 | } 344 | }, 345 | i); 346 | } 347 | for (auto& thread : threads) { 348 | thread.join(); 349 | } 350 | int in = size; 351 | int out = 0; 352 | for (int i = 0; i < nthreadget; i++) { 353 | out += collections[i]; 354 | } 355 | REQUIRE(in == out); 356 | } 357 | 358 | TEST_CASE("Medium parallel test for limited size concurrent queue", 359 | "[Parallel]") { 360 | const int size = 100000; 361 | srand(time(nullptr)); 362 | std::vector buf; 363 | for (int i = 0; i < size; i++) { 364 | buf.push_back(i); 365 | } 366 | 367 | std::random_device rd; 368 | std::mt19937 g(rd()); 369 | std::shuffle(buf.begin(), buf.end(), g); 370 | 371 | ConcurrentQueue q; 372 | const int nthreadput = 10; 373 | const int nthreadget = 10; 374 | 375 | std::vector threads; 376 | 377 | std::atomic completed_put{0}; 378 | for (int i = 0; i < nthreadput; i++) { 379 | int l = i * size / nthreadput; 380 | int r = (i + 1) * size / nthreadput; 381 | threads.emplace_back( 382 | [&](int l, int r) { 383 | for (int i = l; i < r; i++) { 384 | q.Push(buf[i]); 385 | } 386 | if (++completed_put == nthreadput) { 387 | q.SetFinish(); 388 | } 389 | }, 390 | l, r); 391 | } 392 | std::vector> collections(nthreadget); 393 | for (int i = 0; i < nthreadget; i++) { 394 | threads.emplace_back( 395 | [&](int id) { 396 | int x; 397 | while (true) { 398 | bool suc = q.Pop(x); 399 | if (suc) { 400 | collections[id].push_back(x); 401 | } else { 402 | break; 403 | } 404 | } 405 | }, 406 | i); 407 | } 408 | for (auto& thread : threads) { 409 | thread.join(); 410 | } 411 | std::multiset in(buf.begin(), buf.end()); 412 | std::multiset out; 413 | for (int i = 0; i < nthreadget; i++) { 414 | out.insert(collections[i].begin(), collections[i].end()); 415 | } 416 | REQUIRE(in == out); 417 | } 418 | 419 | TEST_CASE("Medium parallel test for unlimited size concurrent queue", 420 | "[Parallel]") { 421 | const int size = 100000; 422 | srand(time(nullptr)); 423 | std::vector buf; 424 | for (int i = 0; i < size; i++) { 425 | buf.push_back(i); 426 | } 427 | 428 | std::random_device rd; 429 | std::mt19937 g(rd()); 430 | std::shuffle(buf.begin(), buf.end(), g); 431 | 432 | ConcurrentQueue q; 433 | const int nthreadput = 10; 434 | const int nthreadget = 10; 435 | 436 | std::vector threads; 437 | 438 | std::atomic completed_put{0}; 439 | for (int i = 0; i < nthreadput; i++) { 440 | int l = i * size / nthreadput; 441 | int r = (i + 1) * size / nthreadput; 442 | threads.emplace_back( 443 | [&](int l, int r) { 444 | for (int i = l; i < r; i++) { 445 | q.Push(buf[i]); 446 | } 447 | if (++completed_put == nthreadput) { 448 | q.SetFinish(); 449 | } 450 | }, 451 | l, r); 452 | } 453 | std::vector> collections(nthreadget); 454 | for (int i = 0; i < nthreadget; i++) { 455 | threads.emplace_back( 456 | [&](int id) { 457 | int x; 458 | while (true) { 459 | bool suc = q.Pop(x); 460 | if (suc) { 461 | collections[id].push_back(x); 462 | } else { 463 | break; 464 | } 465 | } 466 | }, 467 | i); 468 | } 469 | for (auto& thread : threads) { 470 | thread.join(); 471 | } 472 | std::multiset in(buf.begin(), buf.end()); 473 | std::multiset out; 474 | for (int i = 0; i < nthreadget; i++) { 475 | out.insert(collections[i].begin(), collections[i].end()); 476 | } 477 | REQUIRE(in == out); 478 | } 479 | --------------------------------------------------------------------------------