QMutexLocer类可以很方便的简化Qmutex的lock和unlock。因为在复杂的函数中Qmutex的锁进行调试代码是很难的,而QMutexLocker在这些情况下使用可以很好的确保mutex的状态。
下列函数在推出函数时需要解锁的地方有多个,使用QMutex的情况如下,很容易出现忘记解锁的问题,当这个程序的复杂增加时,出现错误的概率也在增大:
int complexFunction(int flag)
{
mutex.lock();
int retVal = 0;
switch (flag) {
case 0:
case 1:
retVal = moreComplexFunction(flag);
break;
case 2:
{
int status = anotherFunction();
if (status < 0) {
mutex.unlock();
return -2;
}
retVal = status + flag;
}
break;
default:
if (flag > 10) {
mutex.unlock();
return -1;
}
break;
}
mutex.unlock();
return retVal;
}
而下列使用QMutexLocker可以极大的简化代码和增加可读性:
int complexFunction(int flag)
{
QMutexLocker locker(&mutex);
int retVal = 0;
switch (flag) {
case 0:
case 1:
return moreComplexFunction(flag);
case 2:
{
int status = anotherFunction();
if (status < 0)
return -2;
retVal = status + flag;
}
break;
default:
if (flag > 10)
return -1;
break;
}
return retVal;
}
当QMutexLocker对象被销毁时(即当函数return时),mutex将会unlock.QMutexLocker也提供了一个mutex()的成员函数用于返回QMutexLocking管理的mutex对象。这个函数在需要访问mutex时很有用,比如QWaitCondition::wait()的情况。例如:
class SignalWaiter
{
private:
QMutexLocker locker;
public:
SignalWaiter(QMutex *mutex)
: locker(mutex)
{
}
void waitForSignal()
{
...
while (!signalled)
waitCondition.wait(locker.mutex());
...
}
};
QT中还提供了QReadLocker和QWriteLocker,用于管理QReadWriteLock,这个和QMutex有些类似,适用于读写线程的保护,这个方法可以实现当一个线程在写文件时,多个线程可以同时读文件,这样可以提高多线程的效率。
3.2 QSemaphore
QSemaphore是QMutex的一个拓展,可以吧保护一定数量的相同资源。与之相比,QMutex只能保护一个资源。比较常用的例子是同步访问生产者和消费者之间的循环缓存:
1)首先看看QSemaphore例子中的全局变量:
const int DataSize = 100000;
const int BufferSize = 8192;
char buffer[BufferSize];
QSemaphore freeBytes(BufferSize);
QSemaphore usedBytes;
DataSize是生产者将生成的数据量。为了使示例尽可能简单,我们将其设为常量。BufferSize是循环缓冲区的大小。它小于DataSize,这意味着在某个时刻,生产者将到达缓冲区的末尾,并从一开始重新启动。
为了同步生产者和消费者,我们需要两个信号量。freebytes Semaphore控制缓冲区的“空闲”区域(生产者尚未填充数据或消费者已读取的区域)。usedBytes Semaphore控制缓冲区的“已用”区域(生产者已填充但消费者尚未读取的区域)。
总之,Semaphore确保生产者永远不会超过消费者前面的bufferSize字节,并且消费者永远不会读取生产者尚未生成的数据。
freebytes Semaphore是用bufferSize初始化的,因为最初整个缓冲区是空的。usedBytes Semaphore初始化为0(如果未指定,则为默认值)。
2)生产者
class Producer : public QThread
{
public:
void run() override
{
qsrand(QTime(0,0,0).secsTo(QTime::currentTime()));
for (int i = 0; i < DataSize; ++i) {
freeBytes.acquire();
buffer[i % BufferSize] = "ACGT"[(int)qrand() % 4];
usedBytes.release();
}
}
};
生产者生成数据量为DataSize的数据。在将字节写入循环缓冲区之前,必须使用freebytes semaphore获取“free”字节。如果使用者没有跟上生产者的步伐,Qsemaphore::acquire()调用可能会阻塞。
最后,生产者使用usedbytes semaphore释放一个字节。“空闲”字节已成功转换为“已用”字节,供消费者读取。
3)消费者
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");
}
signals:
void stringConsumed(const QString &text);
protected:
bool finish;
};
消费者的代码与生产者非常相似,与生产者相反,这次我们获取一个“已用”字节并释放一个“空闲”字节。
4)主函数
在主函数中,我们需要调用QT0hread::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 Semaphore(其初始的available()计数为0)。一旦生产者将一个字节放入缓冲区,freebytes.available()返回bufferSize-1,usedbytes.available()返回1。在这一点上,有两件事可以发生:要么消费者线程接管并读取该字节,要么生产者线程生成第二个字节。
本例中提供的生产者-消费者模型使编写高度并发的多线程应用程序成为可能。在多处理器机器上,由于两个线程可以同时在缓冲区的不同部分上活动,因此该程序的速度可能是等效的基于QMutex的程序的两倍。
但要注意,这些好处并不总是能实现的。获取和释放一个Qsemaphore是有成本的。在实践中,将缓冲区划分为块,并对块(而不是单个字节)进行操作是更有价值的。缓冲区大小也是一个必须根据实验仔细选择的参数。
3.3 QWaitCondition
QWaitCondition 同样可以实现上述的生产者消费者模型,并且比单独使用QMutex更有效率。
1)全局变量
const int DataSize = 100000;
const int BufferSize = 8192;
char buffer[BufferSize];
QWaitCondition bufferNotEmpty;
QWaitCondition bufferNotFull;
QMutex mutex;
int numUsedBytes = 0;
与3.2的QSemaphore的例子相比,为了同步生产者和消费者,我们需要两个QWaitCondition和一个QMutex。当生产者生成了一些数据,BufferNotEmpty QWaitCondition将被发出信号,告诉消费者它可以开始读取数据。当消费者读取了一些数据,BufferNotFull QWaitCondition将被发出信号,并告诉生产者它可以生成更多数据。numusedbytes是缓冲区中包含数据的字节数。
2)生产者
class Producer : public QThread
{
public:
Producer(QObject *parent = NULL) : QThread(parent)
{
}
void run() override
{
qsrand(QTime(0,0,0).secsTo(QTime::currentTime()));
for (int i = 0; i < DataSize; ++i) {
mutex.lock();
if (numUsedBytes == BufferSize)
bufferNotFull.wait(&mutex);
mutex.unlock();
buffer[i % BufferSize] = "ACGT"[(int)qrand() % 4];
mutex.lock();
++numUsedBytes;
bufferNotEmpty.wakeAll();
mutex.unlock();
}
}
};
生产者生成DataSize大小的数据。在向循环缓冲区写入字节之前,必须首先检查缓冲区是否已满(即numusedbytes等于bufferSize)。如果缓冲区已满,线程将在BufferNotFull条件下等待。
最后,生产者增加numusedbytes,并发出条件BufferNotEmpty为true的信号,因为numusedbytes必须大于0。
我们用QMutex锁保护对numusedbytes变量的所有访问。此外,QwaitCondition::wait()函数接受QMutex作为其参数。该QMutex在线程进入等待状态之前解锁,在线程唤醒时锁定。此外,从锁定状态到等待状态的转换是原子的,以防止出现争用条件。
3)消费者
class Consumer : public QThread
{
Q_OBJECT
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");
}
signals:
void stringConsumed(const QString &text);
};
类似的,与生产者相比。消费者在读取字节之前,我们检查缓冲区是否为空(numusedbytes为0),代替生产者检查缓冲区是否满;如果缓冲区为空,则等待BufferNotEmpty条件。读取字节后,我们减少numusedbytes(而不是增加它),并向bufferNotFull条件(而不是bufferNotEmpty条件)发送信号。
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;
}
同样的,因为QMutex解锁与锁定的成本存在,实际运用中也是将缓存区域划分为为块,并对块进行操作更具实际意义。
四、QMutex常见的应用场景
在QT的开发过程中,我们很常需要对某个线程中的工作流程进行暂停的操作,这时就可以通过设置一个暂停的PushButton,加上一个QMutex来实现暂停与继续的功能。如下是对应的槽函数
void MainWindow::pause_or_continue()
{
if(m_state == Running)
{
ui->pushButtonpause->setEnabled(false);
ui->pushButtonpause->setText(QString::fromLocal8Bit("继续"));
pause_mtx.lock(); //全局锁变量,请求锁
m_state = Pauseing;
ui->pushButtonpause->setEnabled(true);
}
else // Pausing
{
ui->pushButtonpause->setText(QString::fromLocal8Bit("暂停"));
m_state = Running;
pause_mtx.unlock();
}
}
其中m_state是一个状态变量 ,在构造函数中设置为m_state=Running
enum STATE{Pauseing, Running} m_state;
pause_mtx是一个全局QMutex对象,应用于工作流程线程中资源的保护。通过信号与槽的连接
connect(ui->pushButtonpause, SIGNAL(clicked(bool)), this, SLOT(pause_or_continue()));
就可以实现通过QMutex来暂停工作流程线程,点击继续则会释放锁。最后需要注意的是当MainWindow被关闭时,如果此时的m_state=Pauseing,并采用quit()来退出线程时,需要将全局锁pause_mtx解开之后再退出采用quit()退出线程,否则应用程序关闭之后,一直在等待锁的释放,就会出现应用程序的进程无法关闭的问题。
MainWindow::~MainWindow()
{
m_worker->stop();
//按下暂停键之后,需要先把锁解除,才能正常关闭工作线程,否则出现进出无法关闭的问题
if(m_state == Pauseing)
pause_mtx.unlock();
workerThread.quit();
workerThread.wait();
qDebug()<<("here!");
}
另外,如果workThread采用terminate()函数终止线程,则锁会被强制终止,但是这样使用terminate()是有风险的,不推荐这样做。