├── .clang-format ├── .github └── workflows │ └── ccpp.yml ├── .gitignore ├── CMakeLists.txt ├── LICENSE ├── README.md ├── include └── rigtorp │ └── MPMCQueue.h ├── mpmc.odg ├── mpmc.png └── src ├── MPMCQueueExample.cpp └── MPMCQueueTest.cpp /.clang-format: -------------------------------------------------------------------------------- 1 | BasedOnStyle: LLVM -------------------------------------------------------------------------------- /.github/workflows/ccpp.yml: -------------------------------------------------------------------------------- 1 | name: C/C++ CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build-ubuntu: 7 | 8 | runs-on: ubuntu-latest 9 | strategy: 10 | matrix: 11 | config: [Debug, Release] 12 | standard: [11, 17] 13 | 14 | steps: 15 | - uses: actions/checkout@v1 16 | - name: Build & Test 17 | run: | 18 | cmake -E remove_directory build 19 | cmake -B build -S . -DCMAKE_BUILD_TYPE=${{ matrix.config }} -DCMAKE_CXX_STANDARD=${{ matrix.standard }} -DCMAKE_CXX_FLAGS="-Werror -fsanitize=address,undefined" 20 | cmake --build build 21 | cd build 22 | ctest --output-on-failure 23 | 24 | build-windows: 25 | 26 | runs-on: windows-latest 27 | strategy: 28 | matrix: 29 | config: [Debug, Release] 30 | standard: [11, 17] 31 | 32 | steps: 33 | - uses: actions/checkout@v1 34 | - name: Build & Test 35 | run: | 36 | cmake -E remove_directory build 37 | cmake -B build -S . -DCMAKE_CXX_STANDARD=${{ matrix.standard }} # -DCMAKE_CXX_FLAGS="/WX" 38 | cmake --build build --config ${{ matrix.config }} 39 | cd build 40 | ctest --output-on-failure 41 | 42 | build-macos: 43 | 44 | runs-on: macOS-latest 45 | strategy: 46 | matrix: 47 | config: [Debug, Release] 48 | standard: [11, 17] 49 | 50 | steps: 51 | - uses: actions/checkout@v1 52 | - name: Build & Test 53 | run: | 54 | cmake -E remove_directory build 55 | cmake -B build -S . -DCMAKE_BUILD_TYPE=${{ matrix.config }} -DCMAKE_CXX_STANDARD=${{ matrix.standard }} -DCMAKE_CXX_FLAGS="-Werror -fsanitize=address,undefined" 56 | cmake --build build 57 | cd build 58 | ctest --output-on-failure -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | cmake-build-* 3 | .vs/ 4 | .vscode/ 5 | .idea/ 6 | .project 7 | *~ -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.5) 2 | 3 | project(MPMCQueue VERSION 1.0 LANGUAGES CXX) 4 | 5 | add_library(${PROJECT_NAME} INTERFACE) 6 | add_library(${PROJECT_NAME}::${PROJECT_NAME} ALIAS ${PROJECT_NAME}) 7 | 8 | target_compile_features(${PROJECT_NAME} INTERFACE cxx_std_11) 9 | 10 | target_include_directories(${PROJECT_NAME} INTERFACE 11 | $ 12 | $) 13 | 14 | # Tests and examples 15 | if(CMAKE_CURRENT_SOURCE_DIR STREQUAL CMAKE_SOURCE_DIR) 16 | if (MSVC) 17 | add_compile_options(/permissive- /W4 /wd4172 /wd4324 /wd4530) 18 | else() 19 | add_compile_options(-Wall -Wextra -Wpedantic) 20 | endif() 21 | 22 | find_package(Threads REQUIRED) 23 | 24 | add_executable(MPMCQueueExample src/MPMCQueueExample.cpp) 25 | target_link_libraries(MPMCQueueExample MPMCQueue Threads::Threads) 26 | 27 | add_executable(MPMCQueueTest src/MPMCQueueTest.cpp) 28 | target_link_libraries(MPMCQueueTest MPMCQueue Threads::Threads) 29 | 30 | enable_testing() 31 | add_test(MPMCQueueTest MPMCQueueTest) 32 | endif() 33 | 34 | # Install 35 | include(GNUInstallDirs) 36 | include(CMakePackageConfigHelpers) 37 | 38 | write_basic_package_version_file( 39 | "${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}ConfigVersion.cmake" 40 | COMPATIBILITY SameMajorVersion 41 | ) 42 | 43 | export( 44 | TARGETS ${PROJECT_NAME} 45 | NAMESPACE ${PROJECT_NAME}:: 46 | FILE "${PROJECT_NAME}Config.cmake" 47 | ) 48 | 49 | if(CMAKE_CURRENT_SOURCE_DIR STREQUAL CMAKE_SOURCE_DIR) 50 | install( 51 | DIRECTORY "include/" 52 | DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} 53 | ) 54 | 55 | install( 56 | TARGETS ${PROJECT_NAME} 57 | EXPORT "${PROJECT_NAME}Config" 58 | INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} 59 | ) 60 | 61 | install( 62 | EXPORT "${PROJECT_NAME}Config" 63 | NAMESPACE ${PROJECT_NAME}:: 64 | DESTINATION "${CMAKE_INSTALL_LIBDIR}/cmake/${PROJECT_NAME}" 65 | ) 66 | 67 | install( 68 | FILES "${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}ConfigVersion.cmake" 69 | DESTINATION "${CMAKE_INSTALL_LIBDIR}/cmake/${PROJECT_NAME}" 70 | ) 71 | endif() -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Erik Rigtorp 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 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MPMCQueue.h 2 | 3 | ![C/C++ CI](https://github.com/rigtorp/MPMCQueue/workflows/C/C++%20CI/badge.svg) 4 | [![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/rigtorp/MPMCQueue/master/LICENSE) 5 | 6 | A bounded multi-producer multi-consumer concurrent queue written in C++11. 7 | 8 | It's battle hardened and used daily in production: 9 | - In the [Frostbite game engine](https://www.ea.com/frostbite) developed by 10 | [Electronic Arts](https://www.ea.com/) for the following games: 11 | - [Anthem (2019)](https://www.ea.com/games/anthem) 12 | - [Battlefield V (2018)](https://www.ea.com/games/battlefield/battlefield-5) 13 | - [FIFA 18 (2017)](https://www.easports.com/fifa/fifa-18-cristiano-ronaldo) 14 | - [Madden NFL 18 (2017)](https://www.ea.com/games/madden-nfl/madden-nfl-18) 15 | - [Need for Speed: Payback (2017)](https://www.ea.com/games/need-for-speed/need-for-speed-payback) 16 | - In the low latency trading infrastructure at [Charlesworth 17 | Research](https://www.charlesworthresearch.com/) and [Marquette 18 | Partners](https://www.marquettepartners.com/). 19 | 20 | It's been cited by the following papers: 21 | - Peizhao Ou and Brian Demsky. 2018. Towards understanding the costs of avoiding 22 | out-of-thin-air results. Proc. ACM Program. Lang. 2, OOPSLA, Article 136 23 | (October 2018), 29 pages. DOI: https://doi.org/10.1145/3276506 24 | 25 | ## Example 26 | 27 | ```cpp 28 | MPMCQueue q(10); 29 | auto t1 = std::thread([&] { 30 | int v; 31 | q.pop(v); 32 | std::cout << "t1 " << v << "\n"; 33 | }); 34 | auto t2 = std::thread([&] { 35 | int v; 36 | q.pop(v); 37 | std::cout << "t2 " << v << "\n"; 38 | }); 39 | q.push(1); 40 | q.push(2); 41 | t1.join(); 42 | t2.join(); 43 | ``` 44 | 45 | ## Usage 46 | 47 | - `MPMCQueue(size_t capacity);` 48 | 49 | Constructs a new `MPMCQueue` holding items of type `T` with capacity 50 | `capacity`. 51 | 52 | - `void emplace(Args &&... args);` 53 | 54 | Enqueue an item using inplace construction. Blocks if queue is full. 55 | 56 | - `bool try_emplace(Args &&... args);` 57 | 58 | Try to enqueue an item using inplace construction. Returns `true` on 59 | success and `false` if queue is full. 60 | 61 | - `void push(const T &v);` 62 | 63 | Enqueue an item using copy construction. Blocks if queue is full. 64 | 65 | - `template void push(P &&v);` 66 | 67 | Enqueue an item using move construction. Participates in overload 68 | resolution only if `std::is_nothrow_constructible::value == 69 | true`. Blocks if queue is full. 70 | 71 | - `bool try_push(const T &v);` 72 | 73 | Try to enqueue an item using copy construction. Returns `true` on 74 | success and `false` if queue is full. 75 | 76 | - `template bool try_push(P &&v);` 77 | 78 | Try to enqueue an item using move construction. Participates in 79 | overload resolution only if `std::is_nothrow_constructible::value == true`. Returns `true` on success and `false` if queue 81 | is full. 82 | 83 | - `void pop(T &v);` 84 | 85 | Dequeue an item by copying or moving the item into `v`. Blocks if 86 | queue is empty. 87 | 88 | - `bool try_pop(T &v);` 89 | 90 | Try to dequeue an item by copying or moving the item into 91 | `v`. Return `true` on sucess and `false` if the queue is empty. 92 | 93 | - `ssize_t size();` 94 | 95 | Returns the number of elements in the queue. 96 | 97 | The size can be negative when the queue is empty and there is at least one 98 | reader waiting. Since this is a concurrent queue the size is only a best 99 | effort guess until all reader and writer threads have been joined. 100 | 101 | - `bool empty();` 102 | 103 | Returns true if the queue is empty. 104 | 105 | Since this is a concurrent queue this is only a best effort guess until all 106 | reader and writer threads have been joined. 107 | 108 | All operations except construction and destruction are thread safe. 109 | 110 | ## Implementation 111 | 112 | ![Memory layout](https://github.com/rigtorp/MPMCQueue/blob/master/mpmc.png) 113 | 114 | Enqeue: 115 | 116 | 1. Acquire next write *ticket* from *head*. 117 | 2. Wait for our *turn* (2 * (ticket / capacity)) to write *slot* (ticket % capacity). 118 | 3. Set *turn = turn + 1* to inform the readers we are done writing. 119 | 120 | Dequeue: 121 | 122 | 1. Acquire next read *ticket* from *tail*. 123 | 2. Wait for our *turn* (2 * (ticket / capacity) + 1) to read *slot* (ticket % capacity). 124 | 3. Set *turn = turn + 1* to inform the writers we are done reading. 125 | 126 | 127 | References: 128 | 129 | - *Daniel Orozco, Elkin Garcia, Rishi Khan, Kelly Livingston, and Guang R. Gao*. 2012. Toward high-throughput algorithms on many-core architectures. ACM Trans. Archit. Code Optim. 8, 4, Article 49 (January 2012), 21 pages. DOI: https://doi.org/10.1145/2086696.2086728 130 | - *Dave Dice*. 2014. [PTLQueue : a scalable bounded-capacity MPMC queue](https://blogs.oracle.com/dave/entry/ptlqueue_a_scalable_bounded_capacity). 131 | - *Oleksandr Otenko*. [US 8607249 B2: System and method for efficient concurrent queue implementation](http://www.google.com/patents/US8607249). 132 | - *Massimiliano Meneghin, Davide Pasetto, Hubertus Franke*. 2012. [Performance evaluation of inter-thread communication mechanisms on multicore/multithreaded architectures](http://researcher.watson.ibm.com/researcher/files/ie-pasetto_davide/PerfLocksQueues.pdf). DOI: https://doi.org/10.1145/2287076.2287098 133 | - *Paul E. McKenney*. 2010. [Memory Barriers: a Hardware View for Software Hackers](http://irl.cs.ucla.edu/~yingdi/web/paperreading/whymb.2010.06.07c.pdf). 134 | - *Dmitry Vyukov*. 2014. [Bounded MPMC queue](http://www.1024cores.net/home/lock-free-algorithms/queues/bounded-mpmc-queue). 135 | 136 | ## Testing 137 | 138 | Testing concurrency algorithms is hard. I'm using two approaches to test the 139 | implementation: 140 | 141 | - A single threaded test that the functionality works as intended, 142 | including that the element constructor and destructor is invoked 143 | correctly. 144 | - A multithreaded fuzz test that all elements are enqueued and 145 | dequeued correctly under heavy contention. 146 | 147 | ## TODO 148 | 149 | - [X] Add allocator supports so that the queue could be used with huge pages and 150 | shared memory 151 | - [ ] Add benchmarks and compare to `boost::lockfree::queue` and others 152 | - [ ] Use C++20 concepts instead of `static_assert` if available 153 | - [X] Use `std::hardware_destructive_interference_size` if available 154 | - [ ] Add API for zero-copy deqeue and batch dequeue operations 155 | - [ ] Add `[[nodiscard]]` attributes 156 | 157 | ## About 158 | 159 | This project was created by [Erik Rigtorp](https://rigtorp.se) 160 | <[erik@rigtorp.se](mailto:erik@rigtorp.se)>. 161 | -------------------------------------------------------------------------------- /include/rigtorp/MPMCQueue.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2020 Erik Rigtorp 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | */ 22 | 23 | #pragma once 24 | 25 | #include 26 | #include 27 | #include // offsetof 28 | #include 29 | #include 30 | #include // std::hardware_destructive_interference_size 31 | #include 32 | 33 | #ifndef __cpp_aligned_new 34 | #ifdef _WIN32 35 | #include // _aligned_malloc 36 | #else 37 | #include // posix_memalign 38 | #endif 39 | #endif 40 | 41 | namespace rigtorp { 42 | namespace mpmc { 43 | #if defined(__cpp_lib_hardware_interference_size) && !defined(__APPLE__) 44 | static constexpr size_t hardwareInterferenceSize = 45 | std::hardware_destructive_interference_size; 46 | #else 47 | static constexpr size_t hardwareInterferenceSize = 64; 48 | #endif 49 | 50 | #if defined(__cpp_aligned_new) 51 | template using AlignedAllocator = std::allocator; 52 | #else 53 | template struct AlignedAllocator { 54 | using value_type = T; 55 | 56 | T *allocate(std::size_t n) { 57 | if (n > std::numeric_limits::max() / sizeof(T)) { 58 | throw std::bad_array_new_length(); 59 | } 60 | #ifdef _WIN32 61 | auto *p = static_cast(_aligned_malloc(sizeof(T) * n, alignof(T))); 62 | if (p == nullptr) { 63 | throw std::bad_alloc(); 64 | } 65 | #else 66 | T *p; 67 | if (posix_memalign(reinterpret_cast(&p), alignof(T), 68 | sizeof(T) * n) != 0) { 69 | throw std::bad_alloc(); 70 | } 71 | #endif 72 | return p; 73 | } 74 | 75 | void deallocate(T *p, std::size_t) { 76 | #ifdef _WIN32 77 | _aligned_free(p); 78 | #else 79 | free(p); 80 | #endif 81 | } 82 | }; 83 | #endif 84 | 85 | template struct Slot { 86 | ~Slot() noexcept { 87 | if (turn & 1) { 88 | destroy(); 89 | } 90 | } 91 | 92 | template void construct(Args &&...args) noexcept { 93 | static_assert(std::is_nothrow_constructible::value, 94 | "T must be nothrow constructible with Args&&..."); 95 | new (&storage) T(std::forward(args)...); 96 | } 97 | 98 | void destroy() noexcept { 99 | static_assert(std::is_nothrow_destructible::value, 100 | "T must be nothrow destructible"); 101 | reinterpret_cast(&storage)->~T(); 102 | } 103 | 104 | T &&move() noexcept { return reinterpret_cast(storage); } 105 | 106 | // Align to avoid false sharing between adjacent slots 107 | alignas(hardwareInterferenceSize) std::atomic turn = {0}; 108 | typename std::aligned_storage::type storage; 109 | }; 110 | 111 | template >> 112 | class Queue { 113 | private: 114 | static_assert(std::is_nothrow_copy_assignable::value || 115 | std::is_nothrow_move_assignable::value, 116 | "T must be nothrow copy or move assignable"); 117 | 118 | static_assert(std::is_nothrow_destructible::value, 119 | "T must be nothrow destructible"); 120 | 121 | public: 122 | explicit Queue(const size_t capacity, 123 | const Allocator &allocator = Allocator()) 124 | : capacity_(capacity), allocator_(allocator), head_(0), tail_(0) { 125 | if (capacity_ < 1) { 126 | throw std::invalid_argument("capacity < 1"); 127 | } 128 | // Allocate one extra slot to prevent false sharing on the last slot 129 | slots_ = allocator_.allocate(capacity_ + 1); 130 | // Allocators are not required to honor alignment for over-aligned types 131 | // (see http://eel.is/c++draft/allocator.requirements#10) so we verify 132 | // alignment here 133 | if (reinterpret_cast(slots_) % alignof(Slot) != 0) { 134 | allocator_.deallocate(slots_, capacity_ + 1); 135 | throw std::bad_alloc(); 136 | } 137 | for (size_t i = 0; i < capacity_; ++i) { 138 | new (&slots_[i]) Slot(); 139 | } 140 | static_assert( 141 | alignof(Slot) == hardwareInterferenceSize, 142 | "Slot must be aligned to cache line boundary to prevent false sharing"); 143 | static_assert(sizeof(Slot) % hardwareInterferenceSize == 0, 144 | "Slot size must be a multiple of cache line size to prevent " 145 | "false sharing between adjacent slots"); 146 | static_assert(sizeof(Queue) % hardwareInterferenceSize == 0, 147 | "Queue size must be a multiple of cache line size to " 148 | "prevent false sharing between adjacent queues"); 149 | static_assert( 150 | offsetof(Queue, tail_) - offsetof(Queue, head_) == 151 | static_cast(hardwareInterferenceSize), 152 | "head and tail must be a cache line apart to prevent false sharing"); 153 | } 154 | 155 | ~Queue() noexcept { 156 | for (size_t i = 0; i < capacity_; ++i) { 157 | slots_[i].~Slot(); 158 | } 159 | allocator_.deallocate(slots_, capacity_ + 1); 160 | } 161 | 162 | // non-copyable and non-movable 163 | Queue(const Queue &) = delete; 164 | Queue &operator=(const Queue &) = delete; 165 | 166 | template void emplace(Args &&...args) noexcept { 167 | static_assert(std::is_nothrow_constructible::value, 168 | "T must be nothrow constructible with Args&&..."); 169 | auto const head = head_.fetch_add(1); 170 | auto &slot = slots_[idx(head)]; 171 | while (turn(head) * 2 != slot.turn.load(std::memory_order_acquire)) 172 | ; 173 | slot.construct(std::forward(args)...); 174 | slot.turn.store(turn(head) * 2 + 1, std::memory_order_release); 175 | } 176 | 177 | template bool try_emplace(Args &&...args) noexcept { 178 | static_assert(std::is_nothrow_constructible::value, 179 | "T must be nothrow constructible with Args&&..."); 180 | auto head = head_.load(std::memory_order_acquire); 181 | for (;;) { 182 | auto &slot = slots_[idx(head)]; 183 | if (turn(head) * 2 == slot.turn.load(std::memory_order_acquire)) { 184 | if (head_.compare_exchange_strong(head, head + 1)) { 185 | slot.construct(std::forward(args)...); 186 | slot.turn.store(turn(head) * 2 + 1, std::memory_order_release); 187 | return true; 188 | } 189 | } else { 190 | auto const prevHead = head; 191 | head = head_.load(std::memory_order_acquire); 192 | if (head == prevHead) { 193 | return false; 194 | } 195 | } 196 | } 197 | } 198 | 199 | void push(const T &v) noexcept { 200 | static_assert(std::is_nothrow_copy_constructible::value, 201 | "T must be nothrow copy constructible"); 202 | emplace(v); 203 | } 204 | 205 | template ::value>::type> 208 | void push(P &&v) noexcept { 209 | emplace(std::forward

(v)); 210 | } 211 | 212 | bool try_push(const T &v) noexcept { 213 | static_assert(std::is_nothrow_copy_constructible::value, 214 | "T must be nothrow copy constructible"); 215 | return try_emplace(v); 216 | } 217 | 218 | template ::value>::type> 221 | bool try_push(P &&v) noexcept { 222 | return try_emplace(std::forward

(v)); 223 | } 224 | 225 | void pop(T &v) noexcept { 226 | auto const tail = tail_.fetch_add(1); 227 | auto &slot = slots_[idx(tail)]; 228 | while (turn(tail) * 2 + 1 != slot.turn.load(std::memory_order_acquire)) 229 | ; 230 | v = slot.move(); 231 | slot.destroy(); 232 | slot.turn.store(turn(tail) * 2 + 2, std::memory_order_release); 233 | } 234 | 235 | bool try_pop(T &v) noexcept { 236 | auto tail = tail_.load(std::memory_order_acquire); 237 | for (;;) { 238 | auto &slot = slots_[idx(tail)]; 239 | if (turn(tail) * 2 + 1 == slot.turn.load(std::memory_order_acquire)) { 240 | if (tail_.compare_exchange_strong(tail, tail + 1)) { 241 | v = slot.move(); 242 | slot.destroy(); 243 | slot.turn.store(turn(tail) * 2 + 2, std::memory_order_release); 244 | return true; 245 | } 246 | } else { 247 | auto const prevTail = tail; 248 | tail = tail_.load(std::memory_order_acquire); 249 | if (tail == prevTail) { 250 | return false; 251 | } 252 | } 253 | } 254 | } 255 | 256 | /// Returns the number of elements in the queue. 257 | /// The size can be negative when the queue is empty and there is at least one 258 | /// reader waiting. Since this is a concurrent queue the size is only a best 259 | /// effort guess until all reader and writer threads have been joined. 260 | ptrdiff_t size() const noexcept { 261 | // TODO: How can we deal with wrapped queue on 32bit? 262 | return static_cast(head_.load(std::memory_order_relaxed) - 263 | tail_.load(std::memory_order_relaxed)); 264 | } 265 | 266 | /// Returns true if the queue is empty. 267 | /// Since this is a concurrent queue this is only a best effort guess 268 | /// until all reader and writer threads have been joined. 269 | bool empty() const noexcept { return size() <= 0; } 270 | 271 | private: 272 | constexpr size_t idx(size_t i) const noexcept { return i % capacity_; } 273 | 274 | constexpr size_t turn(size_t i) const noexcept { return i / capacity_; } 275 | 276 | private: 277 | const size_t capacity_; 278 | Slot *slots_; 279 | #if defined(__has_cpp_attribute) && __has_cpp_attribute(no_unique_address) 280 | Allocator allocator_ [[no_unique_address]]; 281 | #else 282 | Allocator allocator_; 283 | #endif 284 | 285 | // Align to avoid false sharing between head_ and tail_ 286 | alignas(hardwareInterferenceSize) std::atomic head_; 287 | alignas(hardwareInterferenceSize) std::atomic tail_; 288 | }; 289 | } // namespace mpmc 290 | 291 | template >> 293 | using MPMCQueue = mpmc::Queue; 294 | 295 | } // namespace rigtorp 296 | -------------------------------------------------------------------------------- /mpmc.odg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rigtorp/MPMCQueue/b9808ede08f26fa9df4df4e081d19cace8f6c6ea/mpmc.odg -------------------------------------------------------------------------------- /mpmc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rigtorp/MPMCQueue/b9808ede08f26fa9df4df4e081d19cace8f6c6ea/mpmc.png -------------------------------------------------------------------------------- /src/MPMCQueueExample.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | int main(int argc, char *argv[]) { 6 | (void)argc, (void)argv; 7 | 8 | using namespace rigtorp; 9 | 10 | MPMCQueue q(10); 11 | auto t1 = std::thread([&] { 12 | int v; 13 | q.pop(v); 14 | std::cout << "t1 " << v << "\n"; 15 | }); 16 | auto t2 = std::thread([&] { 17 | int v; 18 | q.pop(v); 19 | std::cout << "t2 " << v << "\n"; 20 | }); 21 | q.push(1); 22 | q.push(2); 23 | t1.join(); 24 | t2.join(); 25 | 26 | return 0; 27 | } 28 | -------------------------------------------------------------------------------- /src/MPMCQueueTest.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2018 Erik Rigtorp 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | */ 22 | 23 | #undef NDEBUG 24 | 25 | #include 26 | #include 27 | #include 28 | #include 29 | #include 30 | #include 31 | #include 32 | 33 | // TestType tracks correct usage of constructors and destructors 34 | struct TestType { 35 | static std::set constructed; 36 | TestType() noexcept { 37 | assert(constructed.count(this) == 0); 38 | constructed.insert(this); 39 | }; 40 | TestType(const TestType &other) noexcept { 41 | assert(constructed.count(this) == 0); 42 | assert(constructed.count(&other) == 1); 43 | constructed.insert(this); 44 | }; 45 | TestType(TestType &&other) noexcept { 46 | assert(constructed.count(this) == 0); 47 | assert(constructed.count(&other) == 1); 48 | constructed.insert(this); 49 | }; 50 | TestType &operator=(const TestType &other) noexcept { 51 | assert(constructed.count(this) == 1); 52 | assert(constructed.count(&other) == 1); 53 | return *this; 54 | }; 55 | TestType &operator=(TestType &&other) noexcept { 56 | assert(constructed.count(this) == 1); 57 | assert(constructed.count(&other) == 1); 58 | return *this; 59 | } 60 | ~TestType() noexcept { 61 | assert(constructed.count(this) == 1); 62 | constructed.erase(this); 63 | }; 64 | // To verify that alignment and padding calculations are handled correctly 65 | char data[129]; 66 | }; 67 | 68 | std::set TestType::constructed; 69 | 70 | int main(int argc, char *argv[]) { 71 | (void)argc, (void)argv; 72 | 73 | using namespace rigtorp; 74 | 75 | { 76 | MPMCQueue q(11); 77 | assert(q.size() == 0 && q.empty()); 78 | for (int i = 0; i < 10; i++) { 79 | q.emplace(); 80 | } 81 | assert(q.size() == 10 && !q.empty()); 82 | assert(TestType::constructed.size() == 10); 83 | 84 | TestType t; 85 | q.pop(t); 86 | assert(q.size() == 9 && !q.empty()); 87 | assert(TestType::constructed.size() == 10); 88 | 89 | q.pop(t); 90 | q.emplace(); 91 | assert(q.size() == 9 && !q.empty()); 92 | assert(TestType::constructed.size() == 10); 93 | } 94 | assert(TestType::constructed.size() == 0); 95 | 96 | { 97 | MPMCQueue q(1); 98 | int t = 0; 99 | assert(q.try_push(1) == true); 100 | assert(q.size() == 1 && !q.empty()); 101 | assert(q.try_push(2) == false); 102 | assert(q.size() == 1 && !q.empty()); 103 | assert(q.try_pop(t) == true && t == 1); 104 | assert(q.size() == 0 && q.empty()); 105 | assert(q.try_pop(t) == false && t == 1); 106 | assert(q.size() == 0 && q.empty()); 107 | } 108 | 109 | // Copyable only type 110 | { 111 | struct Test { 112 | Test() {} 113 | Test(const Test &) noexcept {} 114 | Test &operator=(const Test &) noexcept { return *this; } 115 | Test(Test &&) = delete; 116 | }; 117 | MPMCQueue q(16); 118 | // lvalue 119 | Test v; 120 | q.emplace(v); 121 | q.try_emplace(v); 122 | q.push(v); 123 | q.try_push(v); 124 | // xvalue 125 | q.push(Test()); 126 | q.try_push(Test()); 127 | } 128 | 129 | // Movable only type 130 | { 131 | MPMCQueue> q(16); 132 | // lvalue 133 | auto v = std::unique_ptr(new int(1)); 134 | // q.emplace(v); 135 | // q.try_emplace(v); 136 | // q.push(v); 137 | // q.try_push(v); 138 | // xvalue 139 | q.emplace(std::unique_ptr(new int(1))); 140 | q.try_emplace(std::unique_ptr(new int(1))); 141 | q.push(std::unique_ptr(new int(1))); 142 | q.try_push(std::unique_ptr(new int(1))); 143 | } 144 | 145 | { 146 | bool throws = false; 147 | try { 148 | MPMCQueue q(0); 149 | } catch (std::exception &) { 150 | throws = true; 151 | } 152 | assert(throws == true); 153 | } 154 | 155 | // Fuzz test 156 | { 157 | const uint64_t numOps = 1000; 158 | const uint64_t numThreads = 10; 159 | MPMCQueue q(numThreads); 160 | std::atomic flag(false); 161 | std::vector threads; 162 | std::atomic sum(0); 163 | for (uint64_t i = 0; i < numThreads; ++i) { 164 | threads.push_back(std::thread([&, i] { 165 | while (!flag) 166 | ; 167 | for (auto j = i; j < numOps; j += numThreads) { 168 | q.push(j); 169 | } 170 | })); 171 | } 172 | for (uint64_t i = 0; i < numThreads; ++i) { 173 | threads.push_back(std::thread([&, i] { 174 | while (!flag) 175 | ; 176 | uint64_t threadSum = 0; 177 | for (auto j = i; j < numOps; j += numThreads) { 178 | uint64_t v; 179 | q.pop(v); 180 | threadSum += v; 181 | } 182 | sum += threadSum; 183 | })); 184 | } 185 | flag = true; 186 | for (auto &thread : threads) { 187 | thread.join(); 188 | } 189 | assert(sum == numOps * (numOps - 1) / 2); 190 | } 191 | 192 | return 0; 193 | } 194 | --------------------------------------------------------------------------------