Reputation: 107
I am trying to implement a thread safe queue using the include in c++17. Few notes: uint8_t is purposely used instead of a boolean. MultiQueue is just a template because I need to hold 3 different types within my program so instead of writing 3 different functions for each type i used a template. All of this is stored in a file called queue_safe.h
Problem: The queue class seems to be locking up and doing absolutely nothing and timing out THEN instead of returning it executes the code under this comment /* if a timeout was not hit, but the queue was empty previously this will execute */. This results in a cant call .front() on an empty queue.
Things to consider: The queue is definitely not empty, as before launching the threads i can call .get() and something will be "couted" to the screen.
What works: single threaded works fine. It will wait 20 seconds if the queue is empty, then it will return.
#pragma once
#include <string>
#include <mutex>
#include <queue>
#include <condition_variable>
#include <chrono>
#include <iostream>
template <typename StoredType>
class SafeQueue {
public:
std::condition_variable ready;
std::mutex queue_lock;
std::queue<StoredType> safe_queue = {};
uint8_t empty();
void put(StoredType& element);
StoredType get();
};
/* must have it in the header file so linker can see it */
template <typename StoredType>
StoredType SafeQueue<StoredType>::get() {
std::unique_lock<std::mutex> condition_lock(queue_lock);
if (safe_queue.empty()) {
if (ready.wait_for(condition_lock, std::chrono::seconds(20)) == std::cv_status::timeout) {
/* timeout was reached, no items left */
return StoredType();
}
/* if a timeout was not hit, but the queue was empty previously this will execute */
StoredType element = safe_queue.front();
safe_queue.pop();
return element;
}
else {
/* not empty, return an element */
StoredType element = safe_queue.front();
safe_queue.pop();
return element;
}
/* do not need a return value here since empty HAS to be either true/false */
}
template <typename StoredType>
void SafeQueue<StoredType>::put(StoredType& element) {
std::lock_guard<std::mutex> put_guard(queue_lock);
safe_queue.push(element);
ready.notify_one();
}
template <typename StoredType>
uint8_t SafeQueue<StoredType>::empty() {
std::lock_guard<std::mutex> empty_guard(queue_lock);
if (safe_queue.size() == 0) {
return 1;
}
return 0;
}
Upvotes: 0
Views: 206
Reputation: 6494
You probably have a deadlock happening in the put
method of SafeQueue<StoredType>
.
It's standard practice to allow the lock_guard to release the mutex before calling condition_variable::notify_one
. Just add a scope level to the lock/push operations:
template <typename StoredType>
void SafeQueue<StoredType>::put(StoredType& element) {
{
std::lock_guard<std::mutex> put_guard(queue_lock);
safe_queue.push(element);
}
ready.notify_one();
}
Doing this avoids a situation where the notified thread immediately locks again.
Do you need to wait 20s in get
? That's rather a long timeout. You might want to reduce that to avoid threads waiting a while. You can also simplify the get
function to include a predicate in the wait_for
call:
template <typename StoredType>
StoredType SafeQueue<StoredType>::get() {
std::unique_lock<std::mutex> condition_lock(queue_lock);
ready.wait_for(condition_lock, std::chrono::seconds(20), [&] { return !safe_queue.empty(); });
if (safe_queue.empty()) {
return StoredType();
}
/* not empty, return an element */
StoredType element = safe_queue.front();
safe_queue.pop();
return element;
}
This has the advantage of protecting against spurious wake-ups with a predicate. Now the wait will only return if the queue is not empty or the timeout occurs.
Upvotes: 0