使用等待条件的生产者与消费者
“使用等待条件的生成者和消费者”示例演示了如何使用QWaitCondition 和QMutex 来控制对生成者线程和消费者线程共享的环形缓冲区的访问。
生产者将数据写入缓冲区,直到到达缓冲区末尾,此时它将从头开始重新写入,覆盖现有数据。消费者线程在数据生成时读取数据,并将其写入标准错误输出。
等待条件使得并发级别能够高于仅使用互斥锁所能达到的水平。如果对缓冲区的访问仅由QMutex 保护,则消费者线程就无法与生产者线程同时访问缓冲区。 然而,两个线程同时处理缓冲区的不同部分并无不妥。
该示例包含两个类:Producer 和Consumer 。它们都继承自QThread 。用于这两个类之间通信的环形缓冲区以及保护它的同步工具都是全局变量。
除了使用 `QWaitCondition ` 和 `QMutex ` 来解决生产者-消费者问题外,另一种方法是使用 `QSemaphore`。这就是“使用信号量的生产者和消费者”示例所采用的方法。
全局变量
首先,让我们回顾一下环形缓冲区及其相关的同步工具:
constexpr int DataSize = 100000;
constexpr int BufferSize = 8192;
QMutex mutex; // protects the buffer and the counter
char buffer[BufferSize];
int numUsedBytes;
QWaitCondition bufferNotEmpty;
QWaitCondition bufferNotFull;DataSize 是生产者将生成的数据量。为了使示例尽可能简单,我们将其设为常量。BufferSize 是环形缓冲区的大小。它小于DataSize ,这意味着生产者最终会到达缓冲区的末尾,并从头开始循环。
为了同步生产者和消费者,我们需要两个等待条件和一个互斥锁。当生产者生成了一些数据时,会触发bufferNotEmpty 条件,告知消费者可以开始读取数据;当消费者读取了一些数据后,会触发bufferNotFull 条件,告知生产者可以继续生成数据。numUsedBytes 表示缓冲区中包含数据的字节数。
等待条件、互斥锁和numUsedBytes 计数器共同确保生产者永远不会比消费者领先超过BufferSize 字节,并且消费者永远不会读取生产者尚未生成的数据。
生产者类
让我们回顾一下Producer 类的代码:
class Producer : public QThread
{
public:
explicit Producer(QObject *parent = nullptr)
: QThread(parent)
{
}
private:
void run() override
{
for (int i = 0; i < DataSize; ++i) {
{
const QMutexLocker locker(&mutex);
while (numUsedBytes == BufferSize)
bufferNotFull.wait(&mutex);
}
buffer[i % BufferSize] = "ACGT"[QRandomGenerator::global()->bounded(4)];
{
const QMutexLocker locker(&mutex);
++numUsedBytes;
bufferNotEmpty.wakeAll();
}
}
}
};生产者生成DataSize 字节的数据。在将一个字节写入环形缓冲区之前,它必须先检查缓冲区是否已满(即numUsedBytes 等于BufferSize )。如果缓冲区已满,线程将等待bufferNotFull 条件。
最后,生产者将numUsedBytes 递增,并通知条件bufferNotEmpty 为真,因为numUsedBytes 必然大于0。
我们使用互斥锁保护对numUsedBytes 变量的所有访问。此外,QWaitCondition::wait()函数将互斥锁作为其参数。在线程进入睡眠状态之前会解锁该互斥锁,在线程醒来时则会锁定它。此外,从锁定状态到等待状态的转换是原子操作,以防止发生竞争条件。
消费者类
接下来我们来看Consumer 类:
class Consumer : public QThread
{
public:
explicit Consumer(QObject *parent = nullptr)
: QThread(parent)
{
}
private:
void run() override
{
for (int i = 0; i < DataSize; ++i) {
{
const QMutexLocker locker(&mutex);
while (numUsedBytes == 0)
bufferNotEmpty.wait(&mutex);
}
fprintf(stderr, "%c", buffer[i % BufferSize]);
{
const QMutexLocker locker(&mutex);
--numUsedBytes;
bufferNotFull.wakeAll();
}
}
fprintf(stderr, "\n");
}
};该代码与生产者非常相似。在读取字节之前,我们会检查缓冲区是否为空(numUsedBytes 为 0),而不是检查它是否已满;如果为空,则在bufferNotEmpty 条件上等待。 读取字节后,我们将numUsedBytes 减1(而不是加1),并触发bufferNotFull 条件(而不是bufferNotEmpty 条件)。
main() 函数
在main() 中,我们创建了两个线程,并调用QThread::wait()以确保两个线程都有时间完成操作,然后才退出:
int main(int argc, char *argv[])
{
QCoreApplication app(argc, argv);
Producer producer;
Consumer consumer;
producer.start();
consumer.start();
producer.wait();
consumer.wait();
return 0;
}那么,运行程序时会发生什么呢?最初,只有生产者线程可以执行操作;消费者线程被阻塞,等待bufferNotEmpty 条件被触发(此时numUsedBytes 为0)。一旦生产者将一个字节写入缓冲区,numUsedBytes 将严格大于0,此时bufferNotEmpty 条件被触发。 此时可能发生两种情况:要么消费者线程接管并读取该字节,要么生产者继续生产第二个字节。
本例中介绍的生产者-消费者模型使得编写高度并发的多线程应用程序成为可能。在多处理器机器上,该程序的速度最高可达同等基于互斥锁(mutex)的程序的两倍,因为两个线程可以同时在缓冲区的不同部分进行操作。
但需注意,这些优势并非总能实现。对QMutex 进行加锁和解锁是有代价的。实际上,将缓冲区划分为多个块并按块进行操作,而非逐字节处理,通常更为可取。缓冲区大小也是一个必须通过实验谨慎选择的参数。
© 2026 The Qt Company Ltd. Documentation contributions included herein are the copyrights of their respective owners. The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation. Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property of their respective owners.