❝演示Qt多线程的信号量操作编程。❞
生产者将数据写入缓冲区,直到到达缓冲区末尾为止,然后从头开始重新开始,覆盖现有数据。使用者线程读取生成的数据,并将其写入标准错误。
信号量比互斥量可以具有更高的并发级别。如果对缓冲区的访问由QMutex
保护,则使用者线程无法与生产者线程同时访问缓冲区。但是,使两个线程同时在缓冲区的不同部分上工作并没有什么害处。
该示例包括两个类:Producer
和Consumer
。两者都继承自QThread
。用于在这两个类之间进行通信的循环缓冲区以及保护它的信号量是全局变量。
使用QSemaphore
解决生产者-消费者问题的替代方法是使用QWaitCondition
和QMutex
。如需看更多请查看Qt的"Wait Conditions Example
"示例。
让我们从回顾循环缓冲区和相关的信号量开始:
const int DataSize = 100000;
const int BufferSize = 8192;
char buffer[BufferSize];
QSemaphore freeBytes(BufferSize);
QSemaphore usedBytes;
DataSize
是生产者将生成的大量数据。为了使示例尽可能简单,我们将其设为常量。BufferSize
是循环缓冲区的大小。它小于DataSize
,表示生产者将在某个时候到达缓冲区的末尾并从头开始重新启动。
为了使生产者和消费者同步,我们需要两个信号量。该freeBytes
信号控制缓冲的"自由"区域(该区域的生产者还没有装满数据或消费者已经读取了)。useBytes
信号量控制缓冲区的"已用"区域(生产者已填充但使用者尚未读取的区域)。
信号量共同确保了生产者在使用方之前不会超过BufferSize
字节,并且确保使用方从未读取过生产者尚未生成的数据。
freeBytes
信号量使用BufferSize
初始化,因为最初整个缓冲区为空。useBytes
信号量初始化为0
(如果未指定默认值)。
让我们回顾一下Producer该类的代码:
class Producer : public QThread
{
public:
void run() override
{
for (int i = 0; i < DataSize; ++i) {
freeBytes.acquire();
buffer[i % BufferSize] = "ACGT"[QRandomGenerator::global()->bounded(4)];
usedBytes.release();
}
}
};
生产者生成DataSize
字节数据。在将字节写入循环缓冲区之前,它必须使用freeBytes
信号量获取"空闲"字节。如果消费者没有跟上生产者的步伐,调用QSemaphore::acquire()
将会在这里阻塞。
最后,生产者使用usedBytes
信号量释放一个字节。"空闲"字节已成功转换为"已使用"字节,供消费者读取。
现在转到Consumer类:
class Consumer : public QThread
{
Q_OBJECT
public:
void run() override
{
for (int i = 0; i < DataSize; ++i) {
usedBytes.acquire();
fprintf(stderr, "%c", buffer[i % BufferSize]);
freeBytes.release();
}
fprintf(stderr, "\n");
}
};
现在转到Consumer类:
class Consumer : public QThread
{
Q_OBJECT
public:
void run() override
{
for (int i = 0; i < DataSize; ++i) {
usedBytes.acquire();
fprintf(stderr, "%c", buffer[i % BufferSize]);
freeBytes.release();
}
fprintf(stderr, "\n");
}
};
该代码与生产者非常相似,除了这次我们获得一个"已用"字节并释放一个"空闲"字节,而不是相反。
在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;
}
那么当我们运行程序时会发生什么呢?最初,生产者线程是唯一可以做任何事情的线程。消费者被阻止等待usedBytes信号量被释放(其初始available()计数为0)。一旦生产者将一个字节放入缓冲区中,freeBytes.available()
即BufferSize-1
和usedBytes.available()
等于1
。此时,可能会发生两件事:消费者线程接管并读取该字节,或者生产者线程获得第二个字节。
本示例中提供的生产者-消费者
模型使编写高度并发的多线程应用程序成为可能。在多处理器计算机上,该程序的运行速度可能是等效的基于互斥锁的程序的两倍,因为两个线程可以同时在缓冲区的不同部分处于活动状态。
「请注意」,尽管并非总是能实现这些好处的。获取和释放 QSemaphore 需要花费成本。实际上,将缓冲区划分为块并操作块而不是单个字节可能是值得的。缓冲区大小也是必须根据实验仔细选择其参数。
C:\Qt\{你的Qt版本}\Examples\{你的Qt版本}\corelib\threads\semaphores
https://doc.qt.io/qt-5/qtcore-threads-semaphores-example.html