Qt—线程同步之QSemaphore信号量
文章目录
一、简介
信号量是互斥锁的泛化。一个互斥的对象只能被锁定一次,但是可以多次获取一个信号量。信号量通常用于:保护一定数量的相同资源。
二、成员函数API
(2-1)获取n个资源
调用acquire(n)将尝试获取n个资源。如果没有那么多可用的资源,调用将被阻塞,直到出现可用的资源为止。
void QSemaphore::acquire(int n = 1)
(2-2)释放n个资源
调用release(n)函数释放n个资源。
void QSemaphore::release(int n = 1)
(2-3)获取可用资源的数量
调用available()函数返回可用资源的数量。
int QSemaphore::available() const
(2-4)尝试获取n个资源
调用tryAcquire()函数尝试获取资源,如果不能获取资源,它将立即返回。
bool QSemaphore::tryAcquire(int n = 1)
(2-5)在一段时间内尝试获取n个资源
bool QSemaphore::tryAcquire(int n, int timeout)
尝试获取信号量保护的n个资源,成功获取时将返回true。如果available() < n
,这个函数将在超时毫秒时长内等待资源。
注意:如果传递一个负数作为超时参数,就相当于调用acquire()。即如果timeout为负数,这个函数将永远等待直到资源可用,反之,如果资源一直不可用,函数将一直等待下去。
信号量的一个典型应用场景是:控制对由生产者线程和消费者线程共享的循环缓冲区的访问。下文将描述这个例子
三、使用示例
【注】本文代码出自Qt官方示例
【使用示例功能描述】
生产者写入数据到循环缓冲区,直到到达缓冲区的末端后生产者将从头部重新开始进行数据写入,覆盖现有的数据。消费者线程在数据生成时读取数据,并将其打印到标准输出流中(即显示出来)。
信号量比互斥锁有更高级别的并发性。如果对循环缓冲区的访问是由QMutex来保护的,那么消费者线程不能与生产者线程同时访问缓冲区。然而,实际可以让两个线程同时在缓冲区的不同部分上进行操作,不会对缓冲区造成影响。
【示例结构】
这个例子包含两个类:Producer
和Consumer
,两者都继承自QThread
。还包含了用于这两个类之间通信的循环缓冲区和保护循环缓冲区的信号量。循环缓冲区和信号量都是全局的。
(3-1)全局变量
//生产者将生成的数据量
const int DataSize = 100000;
//循环缓冲区的大小,该值小于DataSize
const int BufferSize = 8192;
//循环缓冲区
char buffer[BufferSize];
QSemaphore freeBytes(BufferSize);
QSemaphore usedBytes;
由于BufferSize小于DataSize,这意味着在某一时刻生产者将到达缓冲区的末端并从缓冲区的头部重新开始执行数据写入操作。
为了同步生产者和消费者,定义了两个信号量:
(1)freeBytes
信号量控制缓冲区的“空闲”区域(生产者还没有填满数据或消费者已经读过的区域)。
(2)usedBytes信号量控制缓冲区的“已用”区域(生产者已经填满但消费者还没有读取的区域)。
关系如下图所示:
这些信号量一起确保了生产者永远不会比消费者超前超过BufferSize字节,并且消费者永远不会读取生产者还没有生成的数据。
freeBytes信号量用BufferSize初始化,因为最初整个缓冲区是空的。usedBytes信号量被初始化为0(如果没有指定,则为默认值)。
(3-2)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
信号量获取一个“free”字节。如果消费者没有跟上生产者的步伐,QSemaphore::acquire()
调用可能会阻塞。
最后,生产者使用usedBytes信号量释放一个字节。随后,“free”字节已成功转换为“used”字节,准备供消费者读取啦。
(3-3)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");
}
};
消费者代码类似于生产者线程,实现步骤是:获得一个“used”的字节并释放一个“free”的字节。
(3-4)main函数
int main(int argc, char *argv[])
{
QCoreApplication app(argc, argv);
Producer producer;
Consumer consumer;
producer.start();
consumer.start();
producer.wait();
consumer.wait();
return 0;
}
在main()中,创建两个线程,并调用QThread::wait()来确保两个线程在退出之前都有足够时间保证运行完成。
该程序实际运行效果:
生产者线程是唯一一个可以做任何事情的线程;消费者线程将被阻塞,等待usedBytes信号量被释放(它的初始available()计数是0)。一旦生产者把一个字节放入缓冲区,freeBytes.available()是BufferSize - 1,而usedBytes.available()是1。此时可能会发生两件事:要么由消费者线程接管并读取该字节,要么由生产者线程生成第二个字节。
本例中提供的【生产者-消费者】模型是:编写高并发多线程应用程序的基本模型。在拥有多处理器的机器上,由于两个线程可以同时在缓冲区的不同部分上进行操作,因此程序的运行速度可能是基于互斥锁程序的两倍,这大大提高了程序的运行速度。
注意,获取和释放QSemaphore是有代价的。在实践中,可能需将缓冲区划分为块,并对块进行操作。缓冲区大小参数也是一个必须根据实验仔细选择的参数。