|
Wait Queue
|
This class template allows transferring data between threads with queue semantics (push, pop), using C++ std library general facilities (mutex, condition variable). An internal container is managed within an object of this class template.
Multiple writer and reader threads can access a wait_queue object simultaneously. When a value is pushed on the queue by a writer thread, only one reader thread will be notified to consume the value.
One of the template parameters is the container type, allowing customization for specific use cases (see below for additional details). The default container type is std::deque.
A graceful shutdown can be requested using the request_stop method (modeled on the C++ 20 request_stop from std::stop_source). This allows waiting reader threads to be notified for shutdown. Alternatively a std::stop_token can be passed in to the wait_queue constructor, allowing shutdown from outside of the wait_queue object.
wait_queue uses C++ standard library concurrency facilities (e.g. std::mutex, std::condition_variable_any) in its implementation. It is not a lock-free queue, but it has been designed to be used in memory constrained environments or where deterministic performance is needed.
In particular, wait_queue:
ring_span library for the internal container, as well as Justin Masiulis' circular_buffer library. A "ring buffer" or "circular buffer" uses a fixed size container and implies that the wait_queue can be used in environments where dynamic memory management (heap) is not allowed or is problematic. In particular, no heap memory will be directly allocated within the wait_queue object. A ring_span is a view on a container object instead of directly owning the container, so there are differences in construction and container management.std::mutex locks, etc), as specified by the C++ standard, although this usually indicates an application design issue or issues at the operating system level.noexcept (instead of throwing an exception), every wait_queue method will become noexcept or conditionally noexcept (depending on the type of the data passed through the wait_queue).The only requirement on the type passed through a wait_queue is that it supports either copy construction or move construction. In particular, a default constructor is not required (this is enabled by using std::optional, which does not require a default constructor).
The implementation is adapted from the book Concurrency in Action, Practical Multithreading, by Anthony Williams (2012 edition). The core logic in this library is the same as provided by Anthony in his book, but C++ 20 features have been added, the API is significantly changed and additional features added. The name of the utility class template in Anthony's book is threadsafe_queue.
Each method is fully documented in the class documentation. In particular, function arguments, pre-conditions, and return values are all documented.
Once request_stop has been invoked (either through the wait_queue object or from an external std::stop_source), subsequent pushes will not add any elements to the queue and the push methods will return false.
The push methods return a bool to denote whether a value was succesfully queued or whether a shutdown was requested. The pop methods return a std::optional value. For the wait_and_pop method, if the return value is not present it means a shutdown was requested. For the try_pop method, if the return value is not present it means either the queue was empty at that moment, or that a shutdown was requested.
A std::stop_token can be passed in through the constructors, which allows aa external std::stop_source to request_stop. Alternatively, an internal stop_token will be used, allowing the wait_queue request_stop method to be used to shutdown wait_queue processing.
Once a request_stop is called (either externally or through the wait_queue request_stop) all reader threads calling wait_and_pop are notified, and an empty value returned to those threads. Subsequent calls to push will return a false value.
Example usage, default container:
Example usage with ring buffer (from Martin Moene):
The container type must support the following (depending on which methods are called): default construction, construction with an initial size, push_back (preferably overloaded for both copy and move semantics), emplace_back (with a template parameter pack), front, pop_front, empty, and size. The container must also have a size_type defined. Type constraints and concepts are defined for the various type requirements.
Iterators on a wait_queue are not supported, due to obvious difficulties with maintaining consistency and integrity. The apply method can be used to access the internal data in a threadsafe manner.
Copy and move construction or assignment for the whole queue is disallowed, since the use cases and underlying implications are not clear for those operations. In particular, the exception implications for assigning the internal data from one queue to another is messy, and the general semantics of what it means is not clearly defined. If there is data in one wait_queue that must be copied or moved to another, the apply method can be used or individual push and pop methods called, even if not as efficient as an internal copy or move.
boost circular_buffer can be used for the container type. Memory is allocated only once, at container construction time. This may be useful for environments where construction can use dynamic memory but a push or pop must not allocate or deallocate memory. If the container type is boost circular_buffer then the default constructor for wait_queue cannot be used (since it would result in a container with an empty capacity).Thanks go to Lou Langholtz for adding DBC (Design by Contract) assertions.
Distributed under the Boost Software License, Version 1.0. (See accompanying file LICENSE.txt or copy at http://www.boost.org/LICENSE_1_0.txt)