在上篇已经说过,继承 QThread 类是创建线程的一种方法,另一种就是继承 QObject 类。继承 QObject 类更加灵活。它通过 QObject::moveToThread()方法,将一个 QObeject 的类转移到一个线程里执行,可以通过下图理解。
通过上面的图不难理解,首先我们写一个类继承 QObject,通过 QObject::moveToThread() 方法将它移到一个 QThread 线程里执行。那么可以通过主线程发送信号去调用 QThread 线程的 方法如上图的 fun4(),fun5()等等。这些方法都是在 QThread 线程里执行的。
项目名称:qthread_example2
快速了解继承 QObject 类线程的使用,继承 QObject类的线程。通过 QObject 类继承线程,然后在 MainWindow 类里使用。通过点击一个按钮开启线程。另一个按钮点击关闭线程。另外通过加锁的操作来安全的终止一个线程。(我们可以通过 QMutexLocker 可以安全的使用 QMutex 以免忘记解锁)
我们谈谈为什么需要加锁来终止一个线程?因为 quit()和 exit()方法都不会中途终止线程。 要马上终止一个线程可以用 terminate()方法。但是这个函数存在非常不安全的因素,Qt 官方文档说不推荐使用。
我们可以添加一个 bool 变量,通过主线程修改这个 bool 变量来终止,但是有可能引起访问冲突,所以需要加锁,例程里可能体现不是那么明确,当我们有 doWork1(),doWork2…就能体现到 bool 变量加锁的作用了。但是加锁会消耗一定的性能,增加耗时。
下面的例子是仿照 Qt 官方写的,流程图如下所示。
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include
#include
#include
#include
#include
#include
/* 工人类 */
class Worker;
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = nullptr);
~MainWindow();
private:
/* 开始线程按钮 */
QPushButton *pushButton1;
/* 打断线程按钮 */
QPushButton *pushButton2;
/* 全局线程 */
QThread workerThread;
/* 工人类 */
Worker *worker;
private slots:
/* 按钮1点击开启线程 */
void pushButton1Clicked();
/* 按钮2点击打断线程 */
void pushButton2Clicked();
/* 用于接收工人是否在工作的信号 */
void handleResults(const QString &);
signals:
/* 工人开始工作(做些耗时的操作 ) */
void startWork(const QString &);
};
/* Worker类,这个类声明了doWork1函数,将整个Worker类移至线程workerThread */
class Worker : public QObject
{
Q_OBJECT
private:
/* 互斥锁 */
QMutex lock;
/* 标志位 */
bool isCanRun;
public slots:
/* 耗时的工作都放在槽函数下,工人可以有多份不同的工作,但是每次只能去做一份 */
void doWork1(const QString ¶meter) {
/* 标志位为真 */
isCanRun = true;
/* 死循环 */
while (1) {
/* 此{}作用是QMutexLocker与lock的作用范围,获取锁后,
* 运行完成后即解锁 */
{
QMutexLocker locker(&lock);
/* 如果标志位不为真 */
if (!isCanRun) {
/* 跳出循环 */
break;
}
}
/* 使用QThread里的延时函数,当作一个普通延时 */
QThread::sleep(2);
emit resultReady(parameter + "doWork1函数");
}
/* doWork1运行完成,发送信号 */
emit resultReady("打断doWork1函数");
}
// void doWork2();...
public:
/* 打断线程(注意此方法不能放在槽函数下) */
void stopWork() {
qDebug()<<"打断线程"<
首先,声明一个 Worker 的类继承 QObject 类,这里是参考 Qt 的 QThread 类的帮助文档的写法。将官方的例子运用到我们的例子里去。
然后,我们把耗时的工作都放于槽函数下。工人可以有不同的工作,但是每次只能去做一份。这里不同于继承 QThread 类的线程 run(),继承 QThread 的类只有 run()在新线程里。 而继承 QObject 的类,使用 moveToThread()可以把整个继承的 QObject 类移至线程里执行,所以可以有 doWork1(),doWork2()等耗时的操作,但是这些耗时的操作都应该作为槽函数,由主线程去调用。
最后,进入循环后使用互斥锁判断 isCanRun 标识符的状态,为假即跳出 while 循环,直到 doWork1 结束。注意,虽然 doWork1 结束了,但是线程并没有退出(结束)。因为我们把这个类移到线程里了,直到这个类被销毁。或者使用 quit()和 exit()退出线程才真正的结束!
在源文件“mainwindow.cpp”具体代码如下。
#include "mainwindow.h"
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
{
/* 设置显示位置与大小 */
this->setGeometry(0, 0, 800, 480);
pushButton1 = new QPushButton(this);
pushButton2 = new QPushButton(this);
/* 设置按钮的位置大小 */
pushButton1->setGeometry(300, 200, 80, 40);
pushButton2->setGeometry(400, 200, 80, 40);
/* 设置两个按钮的文本 */
pushButton1->setText("开启线程");
pushButton2->setText("打断线程");
/* 工人类实例化 */
worker = new Worker;
/* 将worker类移至线程workerThread */
worker->moveToThread(&workerThread);
/* 信号槽连接 */
/* 线程完成销毁对象 */
connect(&workerThread, SIGNAL(finished()),
worker, SLOT(deleteLater()));
connect(&workerThread, SIGNAL(finished()),
&workerThread, SLOT(deleteLater()));
/* 发送开始工作的信号,开始工作 */
connect(this, SIGNAL(startWork(QString)),
worker, SLOT(doWork1(QString)));
/* 接收到worker发送过来的信号 */
connect(worker, SIGNAL(resultReady(QString)),
this, SLOT(handleResults(QString)));
/* 点击按钮开始线程 */
connect(pushButton1, SIGNAL(clicked()),
this, SLOT(pushButton1Clicked()));
/* 点击按钮打断线程 */
connect(pushButton2, SIGNAL(clicked()),
this, SLOT(pushButton2Clicked()));
}
MainWindow::~MainWindow()
{
/* 打断线程再退出 */
worker->stopWork();
workerThread.quit();
/* 阻塞线程2000ms,判断线程是否结束 */
if (workerThread.wait(2000)) {
qDebug()<<"线程结束"<startWork(str);
}
void MainWindow::pushButton2Clicked()
{
/* 如果线程在运行 */
if(workerThread.isRunning()) {
/* 停止耗时工作,跳出耗时工作的循环 */
worker->stopWork();
}
}
void MainWindow::handleResults(const QString & results)
{
/* 打印线程的状态 */
qDebug()<<"线程的状态:"<
首先,实例化工人类。继承 QObject 的多线程类不能指定父对象。
工人类实例化后,工人类将自己移至 workerThread 线程里执行。
线程结束后,我们需要使用 deleteLater 来销毁 worker 对象和 workerThread 对象分配的内存。deleteLater 会确认消息循环中没有这两个线程的对象后销毁。
点击开启线程按钮后,应用程序输出窗口每隔 2 秒打印“正在运行 doWork1 函数”,当我们点击打断线程按钮后,窗口打印出“打断 doWork1 函数”。点击打断线程,会打断 doWork1函数的循环,doWork1 函数就运行结束了。再点击开启线程,可以再次运行 doWork1 函数。本例界面简单,仅用了两个按钮和打印语句作为显示部分,但是对初学线程的朋友们友好,因为程序不长。我们可以结合程序的注释,一步步去理解这种线程的写法。重要的是掌握写法,最后才应用到花里胡哨的界面去吧!