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.
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 sends a signal to the worker threads and waits for them to finish. Afterwards, it cleans up and exits.
For the consumer threads, I have written a class that starts a thread upon instantiation. Each class instance has its own FIFO queue into which the producer thread pushes samples. Upon receiving the data, it can immediately start processing as it is not dependent on other threads. When the queue is empty, it waits indefinitely until either a sample is received OR a stop signal is received (e.g. method stopThread
is called). In case the stop signal is received when the queue is not empty yet, it proceeds to finish the remaining elements in the queue and then exits.
The queue implementation that I am using is not complicated to use (https://github.com/cameron314/readerwriterqueue, but I'll give a quick overview of what each used method does:
- try_enqueue: Enqueues a copy of element if there is room in the queue. Returns true if the element was enqueued, false otherwise.
- try_dequeue: Attempts to dequeue an element; if the queue is empty, returns false instead.
- peek: Returns a pointer to the front element in the queue (the one that would be removed next by a call to
try_dequeue
orpop
). If the queue appears empty at the time the method is called, nullptr is returned instead.
Code
#include <chrono>
#include <iostream>
#include <thread>
#include "readerwriterqueue.h"
class Consumer
{
public:
Consumer(int id) : m_BufferQueue(64) {
m_id = id;
m_thread = std::thread([this] {work();});
}
void pushBack(int* num){
while (!m_BufferQueue.try_enqueue(num)){
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
}
void stopThread(){
m_running = false;
if (m_thread.joinable())
m_thread.join();
}
void work() {
m_running = true;
while(m_running || m_BufferQueue.peek())
{
int* received;
bool succeeded = m_BufferQueue.try_dequeue(received);
if(succeeded)
{
std::cout << "Thread " << m_id << " received value " << *received << std::endl;
delete received;
}
else
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
}
private:
int m_id;
moodycamel::ReaderWriterQueue<int*> m_BufferQueue;
std::thread m_thread;
std::atomic_bool m_running;
};
int main() {
Consumer* c1 = new Consumer(1);
Consumer* c2 = new Consumer(2);
// 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);
}
c1->stopThread();
c2->stopThread();
delete c1;
delete c2;
std::cout << "EXIT" << std::endl;
return 0;
}
Points of Concern
- Does the code encapsulate the individual threads in such a way that they are truly independent from each other? I am worried that I might have overlooked something here.
- When stopping the thread, is it good practice to check if it is joinable, before joining it? To my understanding, joinable just checks if the thread has been started, but at that time that should always be the case. So it might be unnecessary.
- Any other oversights that I might have missed?
Link to file "readerwriterqueue.h"