0

I separated the consumer/producer problem from my application to be sure my threads work as they should.

I have one producer thread and a thread pool of consumers: in my application, one thread accepts connections and queue them up (within a custom struct of mine) in one of four queues, and four threads pop from queues and deal with connections queued before; here, my queues will contain random int between 1 and 4, no custom struct.

Four mutex ensure data protection for each queue (plus one mutex for a decent cout on terminal when printing queues size); a priority_queue is used to synchronize the removal from the four queues. The producer thread pushes a new int value in the right queue and then pushes too in priority_queue so that when a thread wants to read he first needs to pop() from priority_queue in order to understand what queue has been pushed (since it is sorted, after some random pushing my priority_queue will look like 1 1 1 2 3 3 3 3 4 4, so a consumer thread would pop(), see the value 1 and understand it has to remove from queue 1).

Why four queues? Because each queue has its own priority (1=max, 4=minimum), elements from queue 1 should be all removed before removing elements from queue 2; same reasonment for all other queues. Since here I have a random pushing of value from 1 to 4, there should be no starvation.

Compiled with: g++ -std=c++11 -o producer-consumer-multiqueue producer-consumer-multiqueue.cpp -pthread on Ubuntu 14.04 x86_64, gcc version 4.8.4.

The problem: aside the strange output due to the scheduler, the consumer threads do not act as I want, since as you can see in the output below it is not given priority to removing elements from queue 1, but removal is done not following the priority (queue 1 max, queue 4 min). I'd like to achieve my goal without using external libraries, no boost et similia.

(0 0 1 0) // (elements in queue 1, in queue 2, in queue 3, in queue 4)
(1 0 1 0)
(1 1 1 0)
(0 0 0 0)
(0 0 0 0)
(0 0 0 0)
(1 0 0 0)
(2 0 0 0)
(2 1 0 0)
(1 1 0 1)
(1 0 0 1)
(1 0 0 0)
(1 0 0 0)
(0 0 0 0)
(1 0 0 0)
...CTRL+c

The code: this is my full testing file, compilable and executable as is:

#include <iostream>
#include <queue>
#include <mutex>
#include <condition_variable>
#include <thread>
#include <random>

using namespace std;

// modify this to modify the number of consumer threads
#define WORKERS_THREADS     4
// max size of each of four queues
#define MAX_QUEUE_SIZE      100
// debug
#define DEFAULTCOLOR        "\033[0m"
#define RED                 "\033[22;31m"
#define YELLOW              "\033[1;33m"
#define GREEN               "\033[0;0;32m"

class MultiQueue {
    public:
        void initThreadPool(void);
        void insert(int num);
        void remove(void);
        void insertPriorityQueue(int num);
        int removePriorityQueue(void);
        void printQueues(string what);
        int getQueue1Size(void);
        int getQueue2Size(void);
        int getQueue3Size(void);
        int getQueue4Size(void);
        int getPrioQueueSize(void);
    private:
        vector<thread> workers;

        queue<int>q1;
        queue<int>q2;
        queue<int>q3;
        queue<int>q4;

        priority_queue<int, vector<int>, greater<int>> prioq;

        // mutex for push/pop in priority queue
        mutex priority_queue_mutex;
        // 4 mutexes for each queue
        mutex m1, m2, m3, m4;
        // mutex for printing 4 queues size
        mutex print;

        // mutex for push/pop to priority_queue
        condition_variable prioq_cond;
        // 4 conds for consumer threads
        condition_variable w1, w2, w3, w4;
};

int MultiQueue::getQueue1Size() { return q1.size(); }

int MultiQueue::getQueue2Size() { return q2.size(); }

int MultiQueue::getQueue3Size() { return q3.size(); }

int MultiQueue::getQueue4Size() { return q4.size(); }

int MultiQueue::getPrioQueueSize() { return prioq.size(); }

void MultiQueue::initThreadPool(void) {
    for (int i=0; i<WORKERS_THREADS; i++) {
        workers.push_back(thread(&MultiQueue::remove, this));
        workers[i].detach();
    }
}

void MultiQueue::printQueues(string what) {
    lock_guard<mutex> l(print);
    if (what == "insert")
        cout << GREEN << '(' << getQueue1Size() << ' ' << getQueue2Size() << ' ' << getQueue3Size() << ' ' << getQueue4Size() << ')' << DEFAULTCOLOR << '\n' << flush;
    else
        cout << YELLOW << '(' << getQueue1Size() << ' ' << getQueue2Size() << ' ' << getQueue3Size() << ' ' << getQueue4Size() << ')' << DEFAULTCOLOR << '\n' << flush;
}

// called from producer thread to tell consumer threads 
// what queues to pop() from
void MultiQueue::insertPriorityQueue(int num) {
    lock_guard<mutex> prio(priority_queue_mutex);
    prioq.push(num);
    prioq_cond.notify_one();
}

// called from consumer threads to see what queues 
// have elements to pop() from
int MultiQueue::removePriorityQueue(void) {
    int ret = 0;
    unique_lock<mutex> prio(priority_queue_mutex);
    prioq_cond.wait(prio, [this] () { return getPrioQueueSize() > 0; });
    ret = prioq.top();
    prioq.pop();
    return ret;
}

// producer thread 
void MultiQueue::insert(int num) {
    switch(num) {
        case 1: {
            unique_lock<mutex> locker(m1);
            w1.wait(locker, [this] () { return getQueue1Size() < MAX_QUEUE_SIZE; });
            q1.push(num);
            break;
        }
        case 2: {
            unique_lock<mutex> locker(m2);
            w2.wait(locker, [this] () { return getQueue2Size() < MAX_QUEUE_SIZE; });
            q2.push(num);
            break;
        }
        case 3: {
            unique_lock<mutex> locker(m3);
            w3.wait(locker, [this] () { return getQueue3Size() < MAX_QUEUE_SIZE; });
            q3.push(num);
            break;      
        }
        case 4: {
            unique_lock<mutex> locker(m4);
            w4.wait(locker, [this] () { return getQueue4Size() < MAX_QUEUE_SIZE; });
            q4.push(num);
            break;
        }
        default: {
            cout << "number not 1, 2, 3 nor 4: " << num << '\n' << flush;
            break;
        }
    }
    printQueues("insert");
    insertPriorityQueue(num);
}

void MultiQueue::remove(void) {
    int which_queue = 0;
    while (true) {
        which_queue = removePriorityQueue();
        switch (which_queue) {
            case 1: {
                lock_guard<mutex> lock(m1);
                int ret = q1.front();
                q1.pop();
                printQueues("remove");
                break;
            }
            case 2: {
                lock_guard<mutex> lock(m2);
                int ret = q2.front();
                q2.pop();
                printQueues("remove");
                break;
            }
            case 3: {
                lock_guard<mutex> lock(m3);
                int ret = q3.front();
                q3.pop();
                printQueues("remove");
                break;
            }
            case 4: {
                lock_guard<mutex> lock(m4);
                int ret = q4.front();
                q4.pop();
                printQueues("remove");
                break;
            }
            default: {
                break;
            }
        }
    }
}

int main(void) {
    int random_num = 0;

    MultiQueue mq;
    mq.initThreadPool();

    default_random_engine eng((random_device())());
    uniform_int_distribution<int> idis(1, 4);
    while (true) {
        random_num = idis(eng);
        mq.insert(random_num);
    }

    return 0;
}
elmazzun
  • 1,066
  • 2
  • 18
  • 44
  • 1
    _"**Why four queues?** Because each queue has its own priority"_ Isn't such rather a job for a single [`std::priority_queue`](http://en.cppreference.com/w/cpp/container/priority_queue)? – πάντα ῥεῖ May 04 '16 at 15:33
  • 1
    I would just use a single `std::priority_queue` but store a priority,data `std::pair` in the queue. The you can sort the queue by the priority part of the pair and you are guaranteed to always consume the highest priority object. – NathanOliver May 04 '16 at 15:37

1 Answers1

0

I see following problems in your code:

  1. Printing doesn't necessarily reflect order in which elements are poped. One thread extract element from queue and then can wait for print lock for a long time and other thread that got element after can be the first that gets print lock.
  2. Similar problem with priority queue. There can be such situation: first thread got element from priority queue and knows that it should pop queue1, then first thread is switched off by scheduler and second thread starts to work. It also pops priority queue and then proceeds to pop queue2 (while first thread is switched off).

I would follow advice from comments and use single priority_queue<std::pair<int,int>>, where first element of std::pair<int,int> is priority and the second one is payload. This would help you to deal with problem 2. As for problem 1 you should print stuff under the same lock as you pop stuff.

Dmitry Ermolov
  • 2,077
  • 1
  • 15
  • 24
  • Done with `priority_queue>` providing it my own class `Compare`, with just one mutex and two condition variables is much simpler to handle. – elmazzun May 06 '16 at 12:55