一、QThread的应用背景
QT是基于事件驱动的架构的,一般在开发QT的应用程序时,为了将耗时的计算工作与主线程分离,以避免界面窗口容易出现卡死或未响应的问题,QThread和QTimer的使用则能很好的实现该类需求。此外,QT的库随着时代的发展,引入了很多有用的函数,比如通过一个全局QMutex来实现工作计算流程的暂停。另外QMutexLocker可以更简单的实现QMutex线程锁的功能,详细可以参看QT5.8或更高版本的QT帮助说明书。这里主要记录了使用QThread时遇到的问题和具体的解决思路。
二、QThread的使用
QThread主要有两种使用方式,第一种是通过继承QObject建立一个包含工作处理信号和槽的Worker类对象,随后采用moveToThread的方式;另一种是直接继承QThread,重写run函数的方式。QT官方更加推荐第一种方式,因为这种方式更加灵活,可以将Worker类的槽跟任何对象、任何其他线程的信号相连接,由于存在队列连接(queued connections)的机制,这种方式在不同线程中的连接是安全的。
QThread的使用需要引入一下头文件:
#include <QThread> //或者#include "QThread"
2.1 QObject方式
以下的代码是帮助文档中,我们可以通过下图的关系来理解,其中的Controller类,一般可以替换成我们QT中MainWindowl类,这样可以实现主线程与新建的线程之间的通讯。
class Worker : public QObject
{
Q_OBJECT
public slots:
void doWork(const QString ¶meter) {
QString result;
/* ... here is the expensive or blocking operation ... */
emit resultReady(result); //发出工作完成的信号
}
signals:
void resultReady(const QString &result);
};
class Controller : public QObject
{
Q_OBJECT
QThread workerThread;
public:
Controller() {
Worker *worker = new Worker;
worker->moveToThread(&workerThread); //将work对象移到一个新的线程中
//线程结束时自动释放线程中的worker对象
connect(&workerThread, &QThread::finished, worker, &QObject::deleteLater);
//Controller的信号与Worker的槽连接
connect(this, &Controller::operate, worker, &Worker::doWork);
//Worker的信号与Controller的槽连接
connect(worker, &Worker::resultReady, this, &Controller::handleResults);
workerThread.start();
}
~Controller() {
//线程的安全退出
workerThread.quit();
workerThread.wait();
}
public slots:
void handleResults(const QString &);
signals:
void operate(const QString &);
};
注意:
线程的退出除了使用quit(),还可以使用terminate(),但是terminate()会立刻强制终止或随后终止,这依赖于操作系统的策略。需要添加wait()函数以确保终止。当这个线程被terminate()终止时,所有等待该线程的线程都会被破坏,因此该函数存在危险并不鼓励使用。此外,采用termininate()终结线程是在任何位置的,比如这就会导致在修改数据时被终止,而线程没有机会去关闭文件和释放锁等。
2.2继承QThread方式
class WorkerThread : public QThread
{
Q_OBJECT
void run() Q_DECL_OVERRIDE {
QString result;
/* ... here is the expensive or blocking operation ... */
emit resultReady(result);
}
signals:
void resultReady(const QString &s);
};
void MyObject::startWorkInAThread()
{
WorkerThread *workerThread = new WorkerThread(this);
connect(workerThread, &WorkerThread::resultReady, this, &MyObject::handleResults);
connect(workerThread, &WorkerThread::finished, workerThread, &QObject::deleteLater);
workerThread->start();
}
采用这种形式的方式很容易出现跨线程调用对象的错误,另外MainWindow中的ui相关控件的设置必须要在主线程中,不然会出现不同程度的错误。这是因为采用这种方式,这个线程在run函数return之后就会结束,除非你调用exec(),否则不会进行事件循环。而跨线程调用对象也可以采用connect()函数的第五个参数,但是这样做需要仔细检查是否安全,所有鼓励采用第一种方式使用QThread.
三、线程的同步
当多个线程需要访问同一个资源时,为了保证计算的正确性需要线程进行顺序化的访问,QT提供了QMutex基础类来实现锁的功能,此外还有QSemaphore,QWaitCondition来实现低层次的线程同步,而采用队列连接的方式则是一个无死锁(死锁指的是A线程在等待B线程的资源释放,B线程也在等待A线程的资源释放,导致A和B线程均在一直等待)的方式。
3.1 QMutex与QMutexLocker
QMutex是一个强制互斥(mutex)的基础类。一个线程通过锁定一个mutex来获得一个共享的资源(变量、文件等),当另一个线程尝试锁定这个资源时,如果这个资源已经被第一个线程锁定,则第二线程就会被阻塞,直到第一个线程使用unlock()释放资源。
下列的例子是用来说明锁的作用的最简单的例子:
int number = 6;
void method1()
{
number *= 5;
number /= 4;
}
void method2()
{
number *= 3;
number /= 2;
}
如果上述的例子正常调用的结果如下:
// method1()
number *= 5; // number is now 30
number /= 4; // number is now 7
// method2()
number *= 3; // number is now 21
number /= 2; // number is now 10
但是如果有两个线程同时访问这两个函数,则会导致如下的可能结果
// Thread 1 calls method1()
number *= 5; // number is now 30
// Thread 2 calls method2().
//
// Most likely Thread 1 has been put to sleep by the operating
// system to allow Thread 2 to run.
number *= 3; // number is now 90
number /= 2; // number is now 45
// Thread 1 finishes executing.
number /= 4; // number is now 11, instead of 10
而使用mutex之后,则可以正常运行:
QMutex mutex;
int number = 6;
void method1()
{
mutex.lock();
number *= 5;
number /= 4;
mutex.unlock();
}
void method2()
{
mutex.lock();
number *= 3;
number /= 2;
mutex.unlock();
}