生产者——消费者的 C++ 代码实现

浅蓝喜灰

本文将重点介绍生产者——消费者模型的 C++ 代码实现。

题目要求

  • 生产者数量限制为 1 个,消费者数量限制为 3 个。
  • 生产者生产消息,将其编号并将其插入到消息队列中。
  • 消费者从消息队列中获取消息,但必须在消息队列不为空时获取。
  • 消息队列可以存放无限多的消息。
  • 程序必须包含图形用户界面。
  • 程序的执行能够随时暂停和继续。

准备工作

我采用 Qt 5.9.9 来实现程序的 GUI 部分,采用 C++ 11 标准中的多线程库实现此程序中的多线程部分。

代码实现

程序代码整体可分为以下若干部分。

  • MainWindow 类
    • 实现 GUI 交互界面
    • 实现消息队列的存储和读取功能
    • 实现程序执行的暂停和继续功能
  • 生产者函数
    • 生产消息
    • 将生产的消息插入到消息队列中
  • 消费者函数
    • 从消息队列中获取消息

MainWindow 类

类声明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class MainWindow : public QMainWindow
{
Q_OBJECT

public:
MainWindow(QWidget *parent = nullptr);
~MainWindow();

void pushTask(int id);
int popTask();

void updateText(int id, int taskID);


private:
Ui::MainWindow *ui;
std::queue<int> queueTask;
};

MainWindow 包含的成员如下

  • 公有成员
    • MainWindow 构造和析构函数
      装载 UI 文件,实现按钮操作,如下图。
    • pushTask 函数
      插入消息到消息队列中。
    • popTask 函数
      从消息队列中获得消息并移除,当队列为空时返回 -1 值。
    • updateText 函数
      将获取消息的文本提示展现在窗口上,如下图。
  • 私有成员
    • ui
      该成员用于装载 ui 文件。
    • queueTask
      该成员用于存储消息队列。

UI 组件

UI 布局如下图。

其中,文本为“开始”的按钮勾选了 checkable,这将让其拥有 checkable 的状态,这是我们实现变换效果的关键。

类定义

MainWindow 需要先装载 ui 文件。

1
ui->setupUi(this);

之后需要将按钮与相应的函数进行绑定,不过,这段代码很长,我将其分成若干部分来向你介绍。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
queueLock.lock();
QObject::connect(ui->start_button, static_cast<void (QPushButton::*)(bool)>(&QPushButton::clicked), this,
[=](bool check){
ui->start_button->setCheckable(!check);
if (check)
{
queueLock.unlock();
ui->start_button->setText("暂停");
}
else
{
queueLock.lock();
ui->start_button->setText("继续");
}
ui->start_button->setEnabled(false);
QTimer::singleShot(800, this, [this]{
ui->start_button->setEnabled(true);
});
});

在这一部分代码中,我们实现了按钮的变化效果。
当程序刚启动时,根据 ui 文件设定,按钮上的文本为“开始”;当我们点下该按钮,就会调用 lambda 函数,这一个函数将按值获取参数 check,该参数来自于按钮的 checkable 状态,它在程序开始时是 true,因此 check 值也为 true,这将满足 if 的判断条件并执行下列代码,该按钮文本被更换为“暂停”。

1
ui->start_button->setText("暂停");

另外,在函数开头,我们使用 setCheckable 函数将按钮的 checkable 状态取反。

1
ui->start_button->setCheckable(!check);

在下次点击中,因为按钮的 checkable 状态已经被取反,因此下一次点击触发的 lambda 函数将获得值为 false 的参数,这导致 if 的条件不能满足,程序转而执行 else 部分,即下方的代码,导致按钮文本更换为为“继续”。

1
ui->start_button->setText("继续");

不过,我们希望实现的不只是按钮文本变化的效果,还包括程序暂停和继续的功能。因此我们在开头先锁定了队列锁 queueLock。

1
queueLock.lock();

这会导致生产者和消费者的线程均被阻塞,则程序无法一启动就执行,必须等到点击开始后才能执行,随后,当我们点击“开始”按钮时,if 部分将调用 unlock 函数来释放队列锁,生产者和消费者不再被阻塞,程序也就开始执行;当我们再次点击时,else 部分将调用 lock 函数锁定队列锁,生产者和消费者被阻塞,实现了暂停的效果。
而下面这部分代码

1
2
3
4
ui->start_button->setEnabled(false);
QTimer::singleShot(800, this, [this]{
ui->start_button->setEnabled(true);
});

将暂时关闭该按钮,并于 0.8 秒后使其再次可用。
接着,我们需要绑定清空按钮。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
QObject::connect(ui->stop_button, static_cast<void (QPushButton::*)(bool)>(&QPushButton::clicked), this, [this]{
queueLock.lock();
ui->pro1_text->clear();
ui->con1_text->clear();
ui->con2_text->clear();
ui->con3_text->clear();
{
std::queue<int> empty;
queueTask.swap(empty);
}
taskCount = 0;
queueLock.unlock();
ui->stop_button->setEnabled(false);
QTimer::singleShot(800, this, [this]{
ui->stop_button->setEnabled(true);
});
});

清空按钮将会需要读写队列,因此需要获取队列锁 queueLock,为了避免在程序暂停时获取队列锁而导致的未定义行为,queueLock 将是递归锁,而不是普通的互斥锁。

1
std::recursive_mutex queueLock;

这允许同一线程可以反复锁定同一把锁,并且不会引发未定义行为。
在锁定队列锁后,我们需要清空文本,这部分的代码很好写,我们只需调用每一个 QTextBrowser 的 clear 方法即可。另外别忘了在清空队列之前,先获取队列锁。

1
2
3
4
5
queueLock.lock();
ui->pro1_text->clear();
ui->con1_text->clear();
ui->con2_text->clear();
ui->con3_text->clear();

仅仅是清空文本还不够,我们还需要清空消息队列,这样一来,当我们暂停程序时,即使消息队列里有消息还没有取出来,也不会影响到重新开始后的消息获取。

1
2
3
4
5
{
std::queue<int> empty;
queueTask.swap(empty);
}
taskCount = 0;

我们在一个单独的语句块中创建一个临时变量 empty,并将其与 queueTask 交换,这样就实现清空消息队列的效果了。
最后,我们需要设置总生产数量为 0,使得下一次生产的消息编号能从 1 开始,而不是从上次中断的位置继续。
随后我们释放锁,并暂时禁用该按钮 0.8 秒。

1
2
3
4
5
queueLock.unlock();
ui->stop_button->setEnabled(false);
QTimer::singleShot(800, this, [this]{
ui->stop_button->setEnabled(true);
});

上面这部分代码设定了两个按钮的一些基本功能,我们还没实现队列的存储和读取功能,好在这并没有什么难的,唯一需要注意的是,popTask()需要先检查消息队列是否为空,若为空则返回 -1,若不为空,则获取消息,并将其从队列中移除。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void MainWindow::pushTask(int id)
{
queueTask.push(id);
}

int MainWindow::popTask()
{
if (queueTask.empty())
{
return -1;
}
int idTask = queueTask.front();
queueTask.pop();
return idTask;
}

最后是文本的展示,我们规定 updateText 函数需要接受调用者的 ID 以及消息的 ID,当调用者 ID 为 0 时代表调用者是生产者,而为 1 到 3 时,则为消费者,插入字符串的时候,将调用者 ID 和消息 ID 拼接到字符串里面即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
void MainWindow::updateText(int id, int taskID)
{
switch (id)
{
case 0:
ui->pro1_text->insertPlainText("生产者生产了商品#");
ui->pro1_text->insertPlainText(QString::number(taskID));
ui->pro1_text->insertPlainText("\n");
break;
case 1:
ui->con1_text->insertPlainText("消费者#1消费了商品#");
ui->con1_text->insertPlainText(QString::number(taskID));
ui->con1_text->insertPlainText("\n");
break;
case 2:
ui->con2_text->insertPlainText("消费者#2消费了商品#");
ui->con2_text->insertPlainText(QString::number(taskID));
ui->con2_text->insertPlainText("\n");
break;
case 3:
ui->con3_text->insertPlainText("消费者#3消费了商品#");
ui->con3_text->insertPlainText(QString::number(taskID));
ui->con3_text->insertPlainText("\n");
break;
default:
break;
}
}

生产者函数

生产者需要源源不断地产生消息并插入到消息队列中,但消息队列可能正在被读取,也可能正在被清空,插入消息后需要将文本输出到展示框,展示框又可能被清空,因此我们在输出和插入消息时是必须获取队列锁的,则实现的代码应当如下。

1
2
3
4
5
6
7
8
9
10
11
12
void producer(MainWindow& w, int id)
{
while (true)
{
queueLock.lock();
++taskCount;
w.pushTask(taskCount);
w.updateText(id, taskCount);
queueLock.unlock();
std::this_thread::sleep_for(std::chrono::milliseconds(600));
}
}

需要注意的是,我们需要在每次消息生产完之后,让其休眠一小会。

消费者函数

每次我们都需要从队列中读取出一个消息并将其从中移除,这意味着这不只是读操作,还是写操作,因此也需要获取队列锁,则实现代码应当如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void consumer(MainWindow& w, int id)
{
while (true)
{
queueLock.lock();
int res = w.popTask();
if (res != -1)
{
w.updateText(id + 1, res);
}
queueLock.unlock();
std::this_thread::sleep_for(std::chrono::milliseconds(500));
}
}

获取消息时,消息队列可能正好是空的(导致返回值变为 -1),因此需要先检查 res 不是 -1 后才能调用 updateText 将其展示出来。
在获取消息后,我们同样需要让消费者线程进行休眠一小会。

主函数部分

我们只需要使用 std::thread 生成 4 个线程,并将其从主进程中分离,这样便可实现生产者——消费者模型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
MainWindow w;
w.setWindowTitle("生产者——消费者程序模拟解决方案");
std::thread pro1(producer, std::ref(w), 0);
std::thread con1(consumer, std::ref(w), 0);
std::thread con2(consumer, std::ref(w), 1);
std::thread con3(consumer, std::ref(w), 2);
pro1.detach();
con1.detach();
con2.detach();
con3.detach();
w.show();
return a.exec();
}
评论