1. Qt常用的线程同步类
QMutex,QMutexLocker,QReadWriteLocker,QReadLocker,QWriteLocker,QSemaphore,QWaitCondition
2. QWaitCondition
2.1 概述
QWaitCondition允许线程告诉其他线程某种条件已经满足。一个或多个线程可以阻塞等待QWaitCondition来使用wakeOne()或wakeAll()设置条件。使用wakeOne()唤醒一个随机选择的线程或wakeAll()唤醒所有线程。
QWaitCondition同样可以使多个线程同时访问同一资源,比单纯使用互斥锁的效率更高,类似于QSemaphore。
2.2 成员函数说明
- QWaitCondition::QWaitCondition()
创建一个条件变量对象 - QWaitCondition::~QWaitCondition()
销毁条件变量对象 - void QWaitCondition::notify_all()
这个函数是为了兼容STL而提供的。它相当于wakeAll()。 - void QWaitCondition::notify_one()
这个函数是为了兼容STL而提供的。它相当于wakeOne()。 - bool QWaitCondition::wait(QMutex *lockedMutex, unsigned long time = ULONG_MAX)
释放lockedMutex并等待条件触发。lockedMutex最初必须由调用线程锁定。如果lockedMutex不处于锁定状态,则该行为未定义。如果lockedMutex是递归互斥,则该函数立即返回。lockedMutex将被解锁,调用线程将阻塞,直到满足以下两个条件之一:
1.另一个线程使用wakeOne()或wakeAll()向它发出信号。在这种情况下,该函数将返回true。
2.已经过了 time 毫秒的时间。如果时间是ULONG_MAX(默认值),那么等待永远不会超时(事件必须发出信号)。如果等待超时,此函数将返回false。
lockedMutex将返回到相同的锁定状态。提供这个函数是为了保证从锁定状态到等待状态的转换是原子的。 - bool QWaitCondition::wait(QReadWriteLock *lockedReadWriteLock, unsigned long time = ULONG_MAX)
释放lockedReadWriteLock并等待条件触发。lockedReadWriteLock最初必须由调用线程锁定。如果lockedReadWriteLock没有处于锁定状态,这个函数会立即返回。lockedReadWriteLock不能被递归锁定,否则该函数将不能正确释放锁。lockedReadWriteLock将被解锁,并且调用线程将阻塞,直到满足以下两个条件之一:
1.另一个线程使用wakeOne()或wakeAll()向它发出信号。在这种情况下,该函数将返回true。
2.已经过了 time 毫秒的时间。如果时间是ULONG_MAX(默认值),那么等待永远不会超时(事件必须发出信号)。如果等待超时,此函数将返回false。
lockedReadWriteLock将返回到相同的锁定状态。提供这个函数是为了保证从锁定状态到等待状态的转换是原子的。 - void QWaitCondition::wakeAll()
唤醒所有处于等待状态的线程。线程被唤醒的顺序取决于操作系统的调度策略,无法控制或预测。 - void QWaitCondition::wakeOne()
唤醒一个在等待条件下的线程。被唤醒的线程取决于操作系统的调度策略,无法控制或预测。
如果您想唤醒一个特定的线程,解决方案通常是使用不同的等待条件,并让不同的线程在不同的条件下等待。
2.3 代码示例
2.3.1 全局变量
const int DataSize = 100000;
const int BufferSize = 8192;
char buffer[BufferSize];
QWaitCondition bufferNotEmpty;
QWaitCondition bufferNotFull;
QMutex mutex;
int numUsedBytes = 0;
- DataSize是生产者将生成的数据量。为了使示例尽可能简单,我们将其设为常数。
- Buffersize是循环缓冲区的大小。它小于DataSize,这意味着在某个时刻生产者将到达缓冲区的末端,并从开始重新启动。
为了同步生产者和消费者,我们需要两个等待条件和一个互斥锁。 - bufferNotEmpty: 当生产者生成一些数据时,就会发出bufferNotEmpty条件信号,告诉消费者可以开始读取数据了。
- bufferNotFull: 当消费者读取了一些数据时,bufferNotFull条件就会发出信号,告诉生产者它可以生成更多的数据。
- mutex 使用互斥量保证对线程操作的原子性
- numUsedBytes是缓冲区中包含数据的字节数,即当前存在多少可用字节。
条件变量、互斥锁和numUsedBytes计数器一起确保生产者永远不会超过消费者BufferSize字节,并且消费者永远不会读取生产者尚未生成的数据。
2.3.2 生产者类 Producer
class Producer : public QThread
{
public:
Producer(QObject *parent = NULL) : QThread(parent)
{
}
void run() override
{
for (int i = 0; i < DataSize; ++i) {
mutex.lock();
if (numUsedBytes == BufferSize)
bufferNotFull.wait(&mutex);
mutex.unlock();
buffer[i % BufferSize] = "ACGT"[QRandomGenerator::global()->bounded(4)];
mutex.lock();
++numUsedBytes;
bufferNotEmpty.wakeAll();
mutex.unlock();
}
}
};
生产者生成DataSize字节的数据。在向循环缓冲区写入字节之前,它必须首先检查缓冲区是否已满(即,numUsedBytes等于BufferSize)。如果缓冲区已满,则线程等待bufferNotFull条件。
最后,生产者增加numUsedBytes,并通知条件bufferNotEmpty为真,因为numUsedBytes必须大于0。
我们用互斥锁保护对numUsedBytes变量的所有访问。另外,QWaitCondition::wait()函数接受一个互斥锁作为它的参数。这个互斥锁在线程进入睡眠状态之前被解锁,在线程醒来时被锁定。此外,从锁定状态到等待状态的转换是原子的,以防止竞态条件的发生。
2.3.3 消费者类 Consumer
class Consumer : public QThread
{
public:
Consumer(QObject *parent = NULL) : QThread(parent)
{
}
void run() override
{
for (int i = 0; i < DataSize; ++i) {
mutex.lock();
if (numUsedBytes == 0)
bufferNotEmpty.wait(&mutex);
mutex.unlock();
fprintf(stderr, "%c", buffer[i % BufferSize]);
mutex.lock();
--numUsedBytes;
bufferNotFull.wakeAll();
mutex.unlock();
}
fprintf(stderr, "\n");
}
};
代码与生产者非常相似。在读取字节之前,我们检查缓冲区是否为空(numUsedBytes为0),而不是缓冲区是否已满,如果缓冲区为空,则等待bufferNotEmpty条件。在读取字节之后,我们减少numUsedBytes(而不是增加它),并发出bufferNotFull条件的信号(而不是bufferNotEmpty条件)。
2.3.4 主函数
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()来确保两个线程在我们退出之前都有时间完成。
那么当我们运行程序时会发生什么呢?最初,生产者线程是唯一可以做任何事情的线程;消费者被阻塞,等待bufferNotEmpty条件被告知(numUsedBytes不为0)。一旦生产者在缓冲区中放入一个字节,numUsedBytes的剩余空间为BufferSize - 1, bufferNotEmpty条件被告知。此时,可能会发生两件事:要么消费者线程接管并读取该字节,要么生产者生成第二个字节。
本例中提供的生产者-消费者模型使得编写高度并发的多线程应用程序成为可能。在多处理器机器上,该程序的速度可能是等效的基于互斥锁的程序的两倍,因为两个线程可以在缓冲区的不同部分同时处于活动状态。
但要意识到,这些好处并不总能实现。锁定和解锁QMutex是有代价的。在实践中,将缓冲区划分为块并对块操作,而不是对单个字节进行操作是更好的处理方法。缓冲区大小也是一个必须根据实际情况仔细选择的参数。
说明来源qt官方文档
代码来源