1. Qt常用的线程同步类
QMutex,QMutexLocker,QReadWriteLocker,QReadLocker,QWriteLocker,QSemaphore,QWaitCondition
2. QSemphore
2.1 概述
信号量是互斥锁的扩展。互斥锁只能锁定一次,但信号量可以多次获取。
信号量通常用于保护一定数量的相同资源,它允许多个线程在同一时刻访问同一资源。
信号量具有比互斥锁更高的并发性。以生产者消费者问题举例,如果对缓冲区的访问由OMutex保护,则消费者线程不能与生产者线程同时访问缓冲区。然而让两个线程同时在缓冲区的不同部分工作并不会产生问题,使用信号量可以实现让生产者和消费者同时操作缓冲区的不同部分,提高效率。
信号量支持两种基本操作,acquire()和release():
- acquire(n)尝试获取n个资源。如果没有那么多可用的资源,调用将阻塞,直到出现足够的可用资源。
- release(n)释放n个资源。
- tryAcquire()函数,如果不能获取资源,它会立即返回。
- available()函数返回资源的数量
举例:
QSemaphore sem(5); // sem.available() == 5
sem.acquire(3); // sem.available() == 2
sem.acquire(2); // sem.available() == 0
sem.release(5); // sem.available() == 5
sem.release(5); // sem.available() == 10
sem.tryAcquire(1); // sem.available() == 9, returns true
sem.tryAcquire(250); // sem.available() == 9, returns false
2.2 成员函数说明
- QSemaphore::QSemaphore(int n = 0)
创建一个新的信号量并将其保护的资源数量初始化为n(默认为0) - QSemaphore::~QSemaphore()
销毁信号量对象 - void QSemaphore::acquire(int n = 1)
尝试获取由信号量保护的n个资源。如果n>available(),则此调用将阻塞,直到有足够的资源可用。 - int QSemaphore::available() const
返回信号量当前可用的资源数量。这个数不可能是负的。 - void QSemaphore::release(int n = 1)
释放n个由信号量保护的资源。
这个函数也可以用来“创建”资源。
举例:QSemaphore sem(5); // a semaphore that guards 5 resources sem.acquire(5); // acquire all 5 resources sem.release(5); // release the 5 resources sem.release(10); // "create" 10 new resources
- bool QSemaphore::tryAcquire(int n = 1)
尝试获取n个由信号量保护的资源,成功时返回true。如果available() < n,此调用立即返回false,而不获取任何资源。
举例:QSemaphore sem(5); // sem.available() == 5 sem.tryAcquire(250); // sem.available() == 5, returns false sem.tryAcquire(3); // sem.available() == 2, returns true
- bool QSemaphore::tryAcquire(int n, int timeout)
尝试获取n个由信号量保护的资源,成功时返回true。如果available() < n,此调用将最多等待超时 timeout 毫秒,以等待资源可用。
举例:QSemaphore sem(5); // sem.available() == 5 sem.tryAcquire(250, 1000); // sem.available() == 5, waits 1000 milliseconds and returns false sem.tryAcquire(3, 30000); // sem.available() == 2, returns true without waiting
2.3 生产者消费者代码实例
2.3.1 全局变量
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(如果没有指定,则为默认值),此时没有数据可供消费。
2.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信号量获取一个“空闲”字节。如果消费者没有跟上生产者的步伐,缓冲区达到BufferSize个字节,代表此时缓冲区已满,freeBytes.available()为0,freeBytes.acquire()调用会阻塞。
最后,生产者使用usedBytes信号量释放一个字节。“空闲”字节已成功转换为“已使用”字节,准备由消费者读取。
2.3.3 消费者类 Consumer
class Consumer : public QThread
{
public:
void run() override
{
for (int i = 0; i < DataSize; ++i) {
usedBytes.acquire();
fprintf(stderr, "%c", buffer[i % BufferSize]);
freeBytes.release();
}
fprintf(stderr, "\n");
}
};
代码类似于生产者,在消费者端我们获得一个“使用”字节并释放一个“空闲”字节。
2.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。此时,可能会发生两件事:要么消费者线程接管并读取该字节,要么生产者线程生成第二个字节。
本例中提供的生产者-消费者模型使得编写高度并发的多线程应用程序成为可能。在多处理器机器上,该程序的速度可能是等效的基于互斥锁的程序的两倍,因为两个线程可以在缓冲区的不同部分同时处于活动状态。
但要意识到,这些好处并不总能实现。获取和释放QSemphore是有代价的。在实践中,将缓冲区划分为块并对块而不是单个字节进行操作是更好的处理方法。缓冲区大小也是一个必须根据实际情况仔细选择的参数。
说明来源qt 官方api
代码来源