Qt多线程基础(一)线程同步之互斥锁同步

时间:2022-12-25 23:18:44

一、直接使用QMutex进行同步

创建线程方法:继承自QThread,重写void run()函数,调用成员start()启动线程,start()中可加入优先级参数。

互斥锁同步方法:void run()函数中使用QMutex来实现同步,当多个线程访问共享变量时,应使用lock/trylock和unlock将对共享变量的操作代码包裹,以保证同步访问共享变量。(C++中引起线程安全的共享资源只有两种:全局变量和静态变量)

示例代码中两个Thread均继承自QThread(),为了保证互斥锁对两个线程均可见,QMutex在一个线程CPP文件中定义,另一个线程文件做extern声明。

示例代码如下:

thread.h

#ifndef MYTHREAD_H
#define MYTHREAD_H
#include <QtCore>
#include <QMutex>

class MyThread:public QThread
{
public:
MyThread(QString name);
void run();
private:
QString mName;

};

#endif // MYTHREAD_H

thread.cpp

#include "mythread.h"
#include <QDebug>

int i=50;
QMutex mutex;
MyThread::MyThread(QString name):QThread(),mName(name)
{
qDebug()<<"creating.."<<endl;
}

void MyThread::run()
{
qDebug()<<this->mName<<"running.."<<endl;
mutex.lock();
/*
for(;i<100;i++)
{
qDebug()<<this->mName<<i<<endl;

}
*/
i++;
i*=2;
qDebug()<<this->mName<<i<<endl;
mutex.unlock();

qDebug()<<this->mName<<"stop running.."<<endl;
sleep(1);

}

thread2.h

#ifndef MYTHREAD2_H
#define MYTHREAD2_H
#include <QThread>
#include <QMutex>
class MyThread2:public QThread
{
public:
MyThread2(QString name);
void run();
private:
QString mName;

};

#endif // MYTHREAD2_H

thread2.cpp

#include "mythread2.h"
#include <QDebug>

extern int i;
extern QMutex mutex;
MyThread2::MyThread2(QString name):QThread(),mName(name)
{
qDebug()<<"creating.."<<endl;
}

void MyThread2::run()
{
qDebug()<<this->mName<<"running.."<<endl;
mutex.lock();
/*
for(;i>0;i--)
{
qDebug()<<this->mName<<i<<endl;
}
*/
i--;
i/=2;
qDebug()<<this->mName<<i<<endl;
mutex.unlock();
qDebug()<<this->mName<<"stop runnning.."<<endl;
sleep(1);
}

main.cpp

#include <QCoreApplication>
#include "mythread.h"
#include "mythread2.h"

int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
MyThread thread1("thread1");

MyThread2 thread2("thread2");

thread1.start(QThread::HighestPriority);//高优先级的任务获得更多的CPU使用比,因此先计算完成
thread2.start();//相对低优先级的任务会后完成

thread1.wait();
qDebug()<<"thread1 is done!"<<endl;
thread1.wait();
qDebug()<<"thread2 is done!"<<endl;

// thread1.exit();
return a.exec();
}

实验结果:

Qt多线程基础(一)线程同步之互斥锁同步


二、使用互斥锁类QMutexLocker(浅谈RAII)


问题:使用QMutex的上锁、解锁操作直接同步会有一个致命缺陷:当代码提前退出时(如函数中多处return或C++抛出异常),可能并未执行unlock(),若其他线程采用lock()阻塞式上锁会一直被阻塞等待释放,导致资源泄露。

解决:根据RAII的思想,我们应该尽量使用对象管理资源,构造时获取互斥锁,析构时释放锁。(参见Effective C++条款13)

具体来讲,QMutexLocker作为一个便利类,可以解决以下两种函数有多个出口的情况:

(1)第一种情况是函数内部多次return,如果直接使用QMutex上锁,必须保证每个return之前都及时释放锁资源(每个return前都要加上unlock()),否则当前线程的run()退出时另一个线程的run()无法获取锁,造成死锁。如下例所示:

  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;
}
可以看到如果使用QMutex进行上锁,在线程的run()函数中调用该函数,一旦该函数在中途return,又没有及时调用unlock()就会导致互斥锁永远没有机会释放。除非在每一个return前加上QMutex的unlock()。

如果我们按上述代码所示,使用QMutexLocker管理QMutex,由于函数中的QMutexLocker是一个局部对象,因此return的时候一定会调用析构并在析构内部完成互斥锁的释放。

(2)另一种情况是C++抛出异常的情况:C++标准里明确规定抛出异常时仍能保证局部对象的析构调用,这也是RAII技术的保证。也就是说由于QMutexLocker是局部对象,所以一旦遇到函数退出时,局部对象被释放都会调用析构,析构内部会释放锁。(参见Effective C++条款29)

至于为何C++抛出异常时仍能保证释放局部对象(栈上变量),这是C++标准规定,请参看:

https://segmentfault.com/q/1010000002498987


因此,Qt提供了互斥锁类QMutexLocker,当QMutexLocker作为局部对象时,函数中途return或抛出异常时均会调用析构释放对象,而该类的析构函数内部调用了参数绑定的QMutex对应的unlock()函数,这也是RAII技术的基础保证。空口无凭,如图为证,这是QMutexLocker内部的析构函数实现:

Qt多线程基础(一)线程同步之互斥锁同步

可以看到,析构里面调用unlock()函数,而unlock()函数内部调用mutex()->unlock(),mutex()是一个常量函数,返回QMutexLocker绑定的QMutex,因此mutex()->unlock()调用了QMutex的unlock()函数,完成了对互斥锁的解锁。如图:

Qt多线程基础(一)线程同步之互斥锁同步

可以看到上方的QMutex类中将QMutexLocker声明成了QMutex的友元!因此QMutexLocker可以在析构中调用QMutex的unlock()完成锁资源的释放。这就使得当run()函数有多个出口退出时(多处return或抛出异常),析构被调用并及时完成互斥锁的释放,从而避免锁资源的泄露问题。

另外一个用到RAII思想的技术比如C++STL的智能指针,也是为了避免堆上空间未及时释放的情况。如果使用普通指针申请堆空间,函数中途抛出异常(比如另一个指针申请空间失败,抛出bad_alloc异常),那该指针申请的空间将无法释放,有人说使用捕获异常在catch中释放所有资源,比如此处泄露的内存,但这并不是个好办法,于是根据RAII思想,智能指针产生了,当智能指针的引用计数减为0时会释放这块内存(delete)。

看到这里,终于放心了吗?这是为什么Qt也推荐使用QMutexLocker的原因:RAII技术可以让我们写出异常安全的代码