|
| 1 | + |
| 2 | +# A single-producer, single-consumer lock-free queue for C++ |
| 3 | + |
| 4 | +This mini-repository has my very own implementation of a lock-free queue (that I designed from scratch) for C++. |
| 5 | + |
| 6 | +It only supports a two-thread use case (one consuming, and one producing). The threads can't switch roles, though |
| 7 | +you could use this queue completely from a single thread if you wish (but that would sort of defeat the purpose!). |
| 8 | + |
| 9 | +Note: If you need a general-purpose multi-producer, multi-consumer lock free queue, I have [one of those too][mpmc]. |
| 10 | + |
| 11 | +This repository also includes a [circular-buffer SPSC queue][circular] which supports blocking on enqueue as well as dequeue. |
| 12 | + |
| 13 | + |
| 14 | +## Features |
| 15 | + |
| 16 | +- [Blazing fast][benchmarks] |
| 17 | +- Compatible with C++11 (supports moving objects instead of making copies) |
| 18 | +- Fully generic (templated container of any type) -- just like `std::queue`, you never need to allocate memory for elements yourself |
| 19 | + (which saves you the hassle of writing a lock-free memory manager to hold the elements you're queueing) |
| 20 | +- Allocates memory up front, in contiguous blocks |
| 21 | +- Provides a `try_enqueue` method which is guaranteed never to allocate memory (the queue starts with an initial capacity) |
| 22 | +- Also provides an `enqueue` method which can dynamically grow the size of the queue as needed |
| 23 | +- Also provides `try_emplace`/`emplace` convenience methods |
| 24 | +- Has a blocking version with `wait_dequeue` |
| 25 | +- Completely "wait-free" (no compare-and-swap loop). Enqueue and dequeue are always O(1) (not counting memory allocation) |
| 26 | +- On x86, the memory barriers compile down to no-ops, meaning enqueue and dequeue are just a simple series of loads and stores (and branches) |
| 27 | + |
| 28 | + |
| 29 | +## Use |
| 30 | + |
| 31 | +Simply drop the readerwriterqueue.h (or readerwritercircularbuffer.h) and atomicops.h files into your source code and include them :-) |
| 32 | +A modern compiler is required (MSVC2010+, GCC 4.7+, ICC 13+, or any C++11 compliant compiler should work). |
| 33 | + |
| 34 | +Note: If you're using GCC, you really do need GCC 4.7 or above -- [4.6 has a bug][gcc46bug] that prevents the atomic fence primitives |
| 35 | +from working correctly. |
| 36 | + |
| 37 | +Example: |
| 38 | + |
| 39 | +```cpp |
| 40 | +using namespace moodycamel; |
| 41 | + |
| 42 | +ReaderWriterQueue<int> q(100); // Reserve space for at least 100 elements up front |
| 43 | + |
| 44 | +q.enqueue(17); // Will allocate memory if the queue is full |
| 45 | +bool succeeded = q.try_enqueue(18); // Will only succeed if the queue has an empty slot (never allocates) |
| 46 | +assert(succeeded); |
| 47 | + |
| 48 | +int number; |
| 49 | +succeeded = q.try_dequeue(number); // Returns false if the queue was empty |
| 50 | + |
| 51 | +assert(succeeded && number == 17); |
| 52 | + |
| 53 | +// You can also peek at the front item of the queue (consumer only) |
| 54 | +int* front = q.peek(); |
| 55 | +assert(*front == 18); |
| 56 | +succeeded = q.try_dequeue(number); |
| 57 | +assert(succeeded && number == 18); |
| 58 | +front = q.peek(); |
| 59 | +assert(front == nullptr); // Returns nullptr if the queue was empty |
| 60 | +``` |
| 61 | +
|
| 62 | +The blocking version has the exact same API, with the addition of `wait_dequeue` and |
| 63 | +`wait_dequeue_timed` methods: |
| 64 | +
|
| 65 | +```cpp |
| 66 | +BlockingReaderWriterQueue<int> q; |
| 67 | +
|
| 68 | +std::thread reader([&]() { |
| 69 | + int item; |
| 70 | +#if 1 |
| 71 | + for (int i = 0; i != 100; ++i) { |
| 72 | + // Fully-blocking: |
| 73 | + q.wait_dequeue(item); |
| 74 | + } |
| 75 | +#else |
| 76 | + for (int i = 0; i != 100; ) { |
| 77 | + // Blocking with timeout |
| 78 | + if (q.wait_dequeue_timed(item, std::chrono::milliseconds(5))) |
| 79 | + ++i; |
| 80 | + } |
| 81 | +#endif |
| 82 | +}); |
| 83 | +std::thread writer([&]() { |
| 84 | + for (int i = 0; i != 100; ++i) { |
| 85 | + q.enqueue(i); |
| 86 | + std::this_thread::sleep_for(std::chrono::milliseconds(10)); |
| 87 | + } |
| 88 | +}); |
| 89 | +writer.join(); |
| 90 | +reader.join(); |
| 91 | +
|
| 92 | +assert(q.size_approx() == 0); |
| 93 | +``` |
| 94 | + |
| 95 | +Note that `wait_dequeue` will block indefinitely while the queue is empty; this |
| 96 | +means care must be taken to only call `wait_dequeue` if you're sure another element |
| 97 | +will come along eventually, or if the queue has a static lifetime. This is because |
| 98 | +destroying the queue while a thread is waiting on it will invoke undefined behaviour. |
| 99 | + |
| 100 | +The blocking circular buffer has a fixed number of slots, but is otherwise quite similar to |
| 101 | +use: |
| 102 | + |
| 103 | +```cpp |
| 104 | +BlockingReaderWriterCircularBuffer<int> q(1024); // pass initial capacity |
| 105 | + |
| 106 | +q.try_enqueue(1); |
| 107 | +int number; |
| 108 | +q.try_dequeue(number); |
| 109 | +assert(number == 1); |
| 110 | + |
| 111 | +q.wait_enqueue(123); |
| 112 | +q.wait_dequeue(number); |
| 113 | +assert(number == 123); |
| 114 | + |
| 115 | +q.wait_dequeue_timed(number, std::chrono::milliseconds(10)); |
| 116 | +``` |
| 117 | +
|
| 118 | +
|
| 119 | +## CMake |
| 120 | +### Using targets in your project |
| 121 | +Using this project as a part of an existing CMake project is easy. |
| 122 | +
|
| 123 | +In your CMakeLists.txt: |
| 124 | +``` |
| 125 | +include(FetchContent) |
| 126 | + |
| 127 | +FetchContent_Declare( |
| 128 | + readerwriterqueue |
| 129 | + GIT_REPOSITORY https://github.com/cameron314/readerwriterqueue |
| 130 | + GIT_TAG master |
| 131 | +) |
| 132 | + |
| 133 | +FetchContent_MakeAvailable(readerwriterqueue) |
| 134 | + |
| 135 | +add_library(my_target main.cpp) |
| 136 | +target_link_libraries(my_target PUBLIC readerwriterqueue) |
| 137 | +``` |
| 138 | +
|
| 139 | +In main.cpp: |
| 140 | +```cpp |
| 141 | +#include <readerwriterqueue.h> |
| 142 | +
|
| 143 | +int main() |
| 144 | +{ |
| 145 | + moodycamel::ReaderWriterQueue<int> q(100); |
| 146 | +} |
| 147 | +``` |
| 148 | + |
| 149 | +### Installing into system directories |
| 150 | +As an alternative to including the source files in your project directly, |
| 151 | +you can use CMake to install the library in your system's include directory: |
| 152 | + |
| 153 | +``` |
| 154 | +mkdir build |
| 155 | +cd build |
| 156 | +cmake .. |
| 157 | +make install |
| 158 | +``` |
| 159 | + |
| 160 | +Then, you can include it from your source code: |
| 161 | +``` |
| 162 | +#include <readerwriterqueue/readerwriterqueue.h> |
| 163 | +``` |
| 164 | + |
| 165 | +## Disclaimers |
| 166 | + |
| 167 | +The queue should only be used on platforms where aligned integer and pointer access is atomic; fortunately, that |
| 168 | +includes all modern processors (e.g. x86/x86-64, ARM, and PowerPC). *Not* for use with a DEC Alpha processor (which has very weak memory ordering) :-) |
| 169 | + |
| 170 | +Note that it's only been tested on x86(-64); if someone has access to other processors I'd love to run some tests on |
| 171 | +anything that's not x86-based. |
| 172 | + |
| 173 | +## More info |
| 174 | + |
| 175 | +See the [LICENSE.md][license] file for the license (simplified BSD). |
| 176 | + |
| 177 | +My [blog post][blog] introduces the context that led to this code, and may be of interest if you're curious |
| 178 | +about lock-free programming. |
| 179 | + |
| 180 | + |
| 181 | +[blog]: http://moodycamel.com/blog/2013/a-fast-lock-free-queue-for-c++ |
| 182 | +[license]: LICENSE.md |
| 183 | +[benchmarks]: http://moodycamel.com/blog/2013/a-fast-lock-free-queue-for-c++#benchmarks |
| 184 | +[gcc46bug]: http://stackoverflow.com/questions/16429669/stdatomic-thread-fence-has-undefined-reference |
| 185 | +[mpmc]: https://github.com/cameron314/concurrentqueue |
| 186 | +[circular]: readerwritercircularbuffer.h |
0 commit comments