使用Qt进行多线程编程的示例
生产者写入数据到缓冲区,当缓冲区写满时再从头开始继续写入覆盖缓冲区。消费者线程读取缓冲区中的数据并写到标准错误输出。
信号量可以具有比互斥锁更高层次的并发。如果访问互斥锁控制的缓冲区,在生产者线程和消费者线程不能同时访问缓冲区。让这两个线程同时访问缓冲区的不同部分是没有问题的。
这个例子由两个类组成:生产者和消费者,他们都继承自QThread。用于在这两个类之间通信的循环缓冲区和保护它的信号量都是全局变量。
使用 QSemaphore 解决生产者-消费者问题的另一种方式是使用QWaitCondition和QMutex,这个方法在Wait Conditions Example中详细讲。
全局变量
const int DataSize = 100000;
const int BufferSize = 8192;
char buffer[BufferSize];
QSemaphore freeBytes(BufferSize);
QSemaphore usedBytes;
DataSize 是生产者要生成的数据总数,为了让这个例子尽可能的简单,我们制定一些常数。
BufferSize
是循环缓冲区的大小,它比 DataSize 要小,也就是说在某一时刻生产者将会用完整个缓冲区兵从缓冲区的开始位置重新写入。
为了同步生产者和消费者,我们需要两个信号量。 freeBytes
信号量控制缓冲区的剩余空间(生产者没有写入火消费者已经读取的部分), usedBytes
信号量控制缓冲区的“已使用”部分(生产者写入后消费者还没有读取的部分)。
同时,这些信号量确保生产者在消费者之前绝不会超过 BufferSize
字节,并且消费者绝不会读取生产者尚未写入的数据。
freeBytes
这个信号量初始化大小为(BufferSize
),因为最初整个缓冲区是空的。 usedBytes
信号量初始化为0(不设置的话默认为0)。
生产者类
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
信号量释放缓冲区的空间。“剩余字节数”会被成功地变成“已使用字节数”,并准备好被消费者读取。
消费者类
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
信号量被释放(初始化计数为0)。一旦生产者在缓冲区中放了一个字节的数据, freeBytes.available()
等于 BufferSize
- 1 并且 usedBytes.available()
为 1。这是,可能发生两件事:消费者线程拿走并读取了这一个字节,或者消费者线程开始写入第二个字节。
这个例子中的生产者-消费者模式实现了高并发多线程的应用。在多进程系统中,这个程序的执行效率可能达到使用互斥锁程序的两倍,因为这两个线程可以同时读取缓冲区中的不同位置。
但要知道这些好处并不是一定会出现,因为获取和释放一个 QSemaphore 是有开销的。将缓冲区分块并在数据块操作而不是逐字节的操作,这可能会进一步提高执行的效率。
以上全文翻译自Qt官方文档 Semaphores Example 。