C ++ Producer Consumer,同一个消费者线程抓取所有任务

时间:2018-01-16 19:17:03

标签: c++ multithreading producer-consumer

我正在用c ++实现一个生产者消费者项目,当我运行该程序时,同一个消费者几乎抓住了所有的工作,而不让任何其他消费者线程抓住任何东西。有时,其他线程确实可以获得一些工作,但是其他线程会控制一段时间。例如,TID 10可以抓住几乎所有的工作,但是突然之间TID 12会抓住它,没有其他消费者线程在其间工作。

知道为什么其他线程没有机会抓住工作?

#include <thread>
#include <iostream>
#include <mutex>
#include <condition_variable>
#include <deque>
#include <csignal>
#include <unistd.h>

using namespace std;

int max_queue_size = 100;
int num_producers = 5;
int num_consumers = 7;
int num_operations = 40;

int operations_created = 0;
thread_local int operations_created_by_this_thread = 0;

int operations_consumed = 0;
thread_local int operations_consumed_by_this_thread = 0;

struct thread_stuff {
    int a;
    int b;
    int operand_num;
    char operand;
};
char operands[] = {'+', '-', '/', '*'};

deque<thread_stuff> q;
bool finished = false;

condition_variable cv;
mutex queue_mutex;

void producer(int n) {
    while (operations_created_by_this_thread < num_operations) {
        int oper_num = rand() % 4;
        thread_stuff equation;
        equation.a = rand();
        equation.b = rand();
        equation.operand_num = oper_num;
        equation.operand = operands[oper_num];


        while ((operations_created - operations_consumed) >= max_queue_size) {
            // don't do anything until it has space available
        }
        {
            lock_guard<mutex> lk(queue_mutex);
            q.push_back(equation);
            operations_created++;
        }
        cv.notify_all();
        operations_created_by_this_thread++;
        this_thread::__sleep_for(chrono::seconds(rand() % 2), chrono::nanoseconds(0));
    }
    {
        lock_guard<mutex> lk(queue_mutex);
        if(operations_created == num_operations * num_producers){
            finished = true;
        }
    }
    cv.notify_all();
}

void consumer() {
    while (true) {
        unique_lock<mutex> lk(queue_mutex);
        cv.wait(lk, [] { return finished || !q.empty(); });
        if(!q.empty()) {
            thread_stuff data = q.front();
            q.pop_front();
            operations_consumed++;
            operations_consumed_by_this_thread++;
            int ans = 0;
            switch (data.operand_num) {
                case 0:
                    ans = data.a + data.b;
                    break;
                case 1:
                    ans = data.a - data.b;
                    break;
                case 2:
                    ans = data.a / data.b;
                    break;
                case 3:
                    ans = data.a * data.b;
                    break;
            }
            cout << "Operation " << operations_consumed << " processed by PID " << getpid()
                 << " TID " << this_thread::get_id() << ": "
                 << data.a << " " << data.operand << " " << data.b << " = " << ans << " queue size: "
                 << (operations_created - operations_consumed) << endl;
        }
        this_thread::yield();
        if (finished) break;
    }
}

void usr1_handler(int signal) {
    cout << "Status: Produced " << operations_created << " operations and "
         << (operations_created - operations_consumed) << " operations are in the queue" << endl;
}

void usr2_handler(int signal) {
    cout << "Status: Consumed " << operations_consumed << " operations and "
         << (operations_created - operations_consumed) << " operations are in the queue" << endl;
}

int main(int argc, char *argv[]) {
    if (argc < 5) {
        cout << "Invalid number of parameters passed in" << endl;
        exit(1);
    }
    max_queue_size = atoi(argv[1]);
    num_operations = atoi(argv[2]);
    num_producers = atoi(argv[3]);
    num_consumers = atoi(argv[4]);

//    signal(SIGUSR1, usr1_handler);
//    signal(SIGUSR2, usr2_handler);
    thread producers[num_producers];
    thread consumers[num_consumers];
    for (int i = 0; i < num_producers; i++) {
        producers[i] = thread(producer, num_operations);
    }
    for (int i = 0; i < num_consumers; i++) {
        consumers[i] = thread(consumer);
    }

    for (int i = 0; i < num_producers; i++) {
        producers[i].join();
    }
    for (int i = 0; i < num_consumers; i++) {
        consumers[i].join();
    }
    cout << "finished!" << endl;
}

3 个答案:

答案 0 :(得分:1)

你一直持有互斥锁 - 包括yield() - 持有互斥锁。

在您的生产者代码中执行unique_lock的范围,从队列中弹出并以原子方式递增计数器。

我看到你有一个最大队列大小。如果队列已满,那么生产者需要第二个条件才能等待,消费者会在消耗物品时发出这种情况。

答案 1 :(得分:1)

  

知道为什么其他线程没有机会抓住工作?

这项民意调查令人不安:

while ((operations_created - operations_consumed) >= max_queue_size) 
{
   // don't do anything until it has space available
}

你可能会在循环中尝试最小的延迟...这是一个“坏邻居”,并且可以“消费”#39;核心。

答案 2 :(得分:0)

您的代码存在一些问题:

使用正常变量进行线程间通信

以下是一个例子:

int operations_created = 0;
int operations_consumed = 0;

void producer(int n) {
    [...]
    while ((operations_created - operations_consumed) >= max_queue_size) { }

以后

void consumer() {
    [...]
    operations_consumed++;

这仅适用于没有优化的x86架构,即-O0。一旦我们尝试启用优化,编译器就会将while循环优化为:

void producer(int n) {
    [...]
    if ((operations_created - operations_consumed) >= max_queue_size) {
        while (true) { }
    }

所以,你的程序只是挂在这里。你可以check this on Compiler Explorer.

  mov eax, DWORD PTR operations_created[rip]
  sub eax, DWORD PTR operations_consumed[rip]
  cmp eax, DWORD PTR max_queue_size[rip]
  jl .L19 // here is the if before the loop
.L20:
  jmp .L20 // here is the empty loop
.L19:

为什么会这样?从单线程程序的角度来看,如果while (condition) { operators }不更改if (condition) while (true) { operators },则operators完全等同于condition

要解决此问题,我们应使用std::atomic<int>代替简单int。这些是为线程间通信而设计的,因此编译器将避免这种优化并生成正确的程序集。

消费者在yield()

时锁定互斥锁

看一下这个片段:

void consumer() {
    while (true) {
        unique_lock<mutex> lk(queue_mutex);
        [...]
        this_thread::yield();
        [...]
    }

基本上这意味着消费者持有yield()持有锁。由于只有一个消费者可以一次锁定(互斥体代表互斥),这就解释了为什么其他消费者无法消费这项工作。

要解决此问题,我们应在queue_mutex之前解锁yield(),即:

void consumer() {
    while (true) {
        {
            unique_lock<mutex> lk(queue_mutex);
            [...]
        }
        this_thread::yield();
        [...]
    }

这仍然不能保证只有一个线程可以完成大部分任务。当我们在生产者中执行notify_all()时,所有线程都会被唤醒,但只有一个会锁定互斥锁。由于我们安排的工作很小,当生产者调用notify_all()我们的线程将完成工作时,完成yield()并准备好接下来的工作。

那么为什么这个线程锁定互斥锁,而不是另一个呢?我想这是由于CPU缓存和忙碌等待而发生的。刚完成工作的线程是“热门”,它在CPU缓存中并准备锁定互斥锁。在进入睡眠状态之前,它也可能会尝试忙于等待互斥几个周期,从而增加了获胜的机会。

要解决此问题,我们可以删除生产者中的睡眠(因此它会更频繁地唤醒其他线程,因此其他线程也会“热”),或者在消费者中执行sleep() yield()的{​​(因此这个线程在睡眠期间变得“冷”)。

无论如何,由于互斥锁,没有机会并行完成这项工作,所以同一个线程完成大部分工作的事实是完全自然的IMO。