I have a thread safe queue in my library c9y. It is generally used as a task queue in the task_pool class, but in can be used for any producer / consumer problem.
#ifndef _C9Y_QUEUE_H_
#define _C9Y_QUEUE_H_
#include <mutex>
#include <condition_variable>
#include <deque>
#include <chrono>
namespace c9y
{
using namespace std::chrono_literals;
//! Thread Safe Queue
//!
//! This is a thread safe implementation of a queue.
//!
//! @note There is no really safe way to copy a queue, so this
//! queue is not copyable or asingable.
template <typename T, class Container = std::deque<T>>
class queue
{
public:
typedef Container container_type;
typedef typename Container::value_type value_type;
typedef typename Container::size_type size_type;
typedef typename Container::reference reference;
typedef typename Container::const_reference const_reference;
//! Create an empty queue.
queue() {}
//! Initialize the queue with values.
//!
//! @param c the container to initialize the queue with
explicit
queue(const Container& c)
: container(c) {}
//! Initialize the queue with a reange of values.
//!
//! @param begin an iterator to the beginning of the range
//! @param end an iterator to the one beond the end of the range
template <class Iterator>
queue(const Iterator& begin, const Iterator& end)
: container(begin, end) {}
//! Destructor
~queue()
{
wake();
}
//! Push a value onto the queue.
//!
//! This method will push the value onto the queue and
//! wake up a thread that is wating in pop_wait.
//!
//! @param value the value to push onto the queue
void push(const value_type& value)
{
auto lock = std::unique_lock<std::mutex>{mutex};
container.push_back(value);
cond.notify_one();
}
//! Pop a value of the queue.
//!
//! This method will try to pop a value off the queue. If no value is
//! in the queue, it will return false.
//!
//! @param value the value of the pop
//! @return true if a value was poped of the queue
bool pop(value_type& value)
{
auto lock = std::unique_lock<std::mutex>{mutex};
if (!container.empty())
{
value = container.front();
container.pop_front();
return true;
}
else
{
return false;
}
}
//! Pop a value of the queue, wait if nessesary.
//!
//! This method will try to pop a value off the queue. If no value is
//! in the queue, it will wait until either a value is pushed onto the
//! queue or wake is called.
//!
//! @param value the value of the pop
//! @return true if a value was poped of the queue
//!
//! @warning It is quite simple to build a race condition with pop_wait
//! and wake. If you intend to reliably wake all waiting threads, use
//! pop_wait_for with a reasonable timeout.
bool pop_wait(value_type& value)
{
auto lock = std::unique_lock<std::mutex>{mutex};
if (container.empty())
{
cond.wait(lock);
}
if (!container.empty())
{
value = container.front();
container.pop_front();
return true;
}
else
{
return false;
}
}
//! Pop a value of the queue, wait for a defined duration if nessesary.
//!
//! This method will try to pop a value off the queue. If no value is
//! in the queue, it will wait until either a value is pushed onto the
//! queue or wake is called.
//!
//! @param value the value of the pop
//! @param duration the duration to wait for
//! @return true if a value was poped of the queue
bool pop_wait_for(value_type& value, std::chrono::milliseconds duration)
{
auto lock = std::unique_lock<std::mutex>{mutex};
if (container.empty())
{
cond.wait_for(lock, duration);
}
if (!container.empty())
{
value = container.front();
container.pop_front();
return true;
}
else
{
return false;
}
}
//! Wake up any threads that are wating in pop_wait.
void wake()
{
cond.notify_all();
}
private:
std::mutex mutex;
std::condition_variable cond;
Container container;
queue(const queue& other) = delete;
queue& operator = (const queue& other) = delete;
};
}
#endif
Of course there are matching unit tests:
#include <c9y/queue.h>
#include <atomic>
#include <thread>
#include <gtest/gtest.h>
#include <c9y/thread_pool.h>
using namespace std::chrono_literals;
TEST(queue, create)
{
auto q = c9y::queue<int>{};
}
TEST(queue, consumer_producer)
{
auto q = c9y::queue<int>{};
auto count = std::atomic<unsigned int>{0};
auto prod = c9y::thread_pool{[&] () {
for (int i = 1; i < 101; i++)
{
q.push(i);
}
}, 3};
auto cons = c9y::thread_pool{[&] () {
int value = 0;
while (q.pop(value))
{
count++;
std::this_thread::sleep_for(1ms);
}
}, 3};
prod.join();
cons.join();
EXPECT_EQ(300, static_cast<unsigned int>(count));
}
TEST(queue, consumer_producer_wait)
{
auto q = c9y::queue<int>{};
auto count = std::atomic<unsigned int>{0};
auto prod = c9y::thread_pool{[&] () {
for (int i = 1; i < 101; i++)
{
q.push(i);
std::this_thread::sleep_for(5ms);
}
}, 3};
auto cons = c9y::thread_pool{[&] () {
int value = 0;
while (q.pop_wait_for(value, 100ms))
{
count++;
std::this_thread::sleep_for(3ms);
}
}, 3};
prod.join();
std::this_thread::sleep_for(100ms);
q.wake();
cons.join();
EXPECT_EQ(300, static_cast<unsigned int>(count));
}
Just by posting it here, I already found some issues I need to look into. This code is C++11 code but the library was recently updated to C++20; so these standards should be applied. I am interested what you think.
sleep
in unit tests like these looks like a bad practice because, being a routine, a test should execute as fast as possible. No need to make iterator parametersconst &
. \$\endgroup\$