This is a follow-up post to the topic discussed here.
Description
I am trying to implement a robust, multi-threaded producer-consumer model with a single producer in the main thread and several consumers in other threads.
After researching the topic, I also added a copy constructor and an assignment operator (or better, deleted it), to satisfy the rule of three. I did so, as being able to copy a thread managing class instance does not make sense.
The operating mode is the same as before, e.g. the generator thread loops over data, recognizes through some condition which sort of data it is dealing with, and proceeds to delegate the sample to the respective worker thread (via pushBack(data)). When reaching the end of the source data, it queues a special value to the worker threads and waits for them to finish. Afterwards, it cleans up and exits.
The queue implementation (found here) provides a blocking dequeue function, e.g. no polling is necessary. The downside is that the consumers do not necessarily register that the main thread has set m_running
(a flag indicating if execution should be stopped) because they are stuck waiting. As mentioned before, this can be bypassed by queueing a special value. In the provided example, I quietly assume that the queue only manages pointer types (e.g. int*). In that case, the special value was determined to be nullptr
.
Credit goes to @G.Sliepen, who provided many suggestions in the previous topic.
Code
#include <chrono>
#include <iostream>
#include <thread>
#include <functional>
#include "readerwriterqueue.h"
template<typename T>
class Consumer
{
public:
Consumer(int id, std::function<void(int, T&)> func) : m_id(id), m_func(func) {}
~Consumer(){
m_running = false;
this->pushBack(nullptr);
m_thread.join();
}
Consumer(const Consumer&) = delete;
Consumer& operator=(const Consumer&) = delete;
Consumer() : m_id(0) {};
void pushBack(const T& t){
m_BufferQueue.enqueue(t);
}
private:
void work() {
m_running = true;
while(m_running || m_BufferQueue.peek())
{
T t;
m_BufferQueue.wait_dequeue(t);
if (t == nullptr)
break;
m_func(m_id, t);
}
std::cout << "EXIT thread " << m_id << std::endl;
}
int m_id;
std::function<void(int, T&)> m_func;
moodycamel::BlockingReaderWriterQueue<T> m_BufferQueue{64};
std::thread m_thread{&Consumer::work, this};
std::atomic_bool m_running;
};
int main() {
auto func = [](int id, int* val){
std::cout << "Thread " << id << " received value " << *val << std::endl;
delete val;
};
Consumer<int*> c1(1, func);
Consumer<int*> c2(2, func);
// data generator
for(int i = 0; i < 10; i++)
{
int* val = new int(i);
if (i % 2 == 0)
c1.pushBack(val);
else
c2.pushBack(val);
}
std::cout << "EXIT" << std::endl;
return 0;
}
Question
This implementation breaks when using a non-pointer datatype in the template. As far as I know, references always have to reference non-null objects, so checking for "null-references" does not make sense. And checking for every possible type seems to be very error-prone. I thought of adding a parametrized member variable that holds the abortion value, but I am not sure if this is the best way of doing it.
In short: is there an elegant way to define an abortion value that is type-agnostic?