文章目录
一、摘要
1.线程基础
在Qt项目中,每个程序都是在线程中工作,进行UI显示、数据处理或者信号与槽的响应等等。程序启动后拥有的第一个线程为程序的主线程,在UI项目中,UI所在的线程即为主线程。
2.为什么要创建多线程
为什么要创建多线程,关于这个问题,先来看一段程序:
#include "mainwindow.h"
#include "ui_mainwindow.h"
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow)
{
ui->setupUi(this);
connect(&newtimer,SIGNAL(timeout()),this,SLOT(timerslot()));
newtimer.start(1000);
}
MainWindow::~MainWindow()
{
delete ui;
}
void MainWindow::on_pushButton_clicked()
{
while(1);
}
void MainWindow::timerslot()
{
second++;
ui->lcdNumber->display(QString::number(second));
newtimer.start();
}
简单的一段定时器计数,UI界面显示的一个小程序,效果如下图所示:
注意到我在显示框的下面加了一个按钮,槽函数中写了一个死循环while(1);
所以,当点击按钮时,出现了下面这个现象:
程序是直接死掉了。。
有人说,程序直接进入while(1)
,死循环,不死才怪。确实,程序之所以死掉就是因为这个
while(1)
,但不仅仅因为他是一个死循环,不要忘了我们main.cpp中还有一句return a.exec();
这句话本身也是个循环,程序为什么没死呢?另外做过嵌入式编程的小伙伴都知道,我们在主函数int main()
中也要加一个while(1)
,然后将要执行的函数放进去,那程序为什么不死呢?
让我们在while(1)
里面加一句话
while(1)
{
qDebug()<<"aaaaaa";
}
再次点击按钮你会发现,虽然出现了“程序未响应”但是应用程序输出窗口qDebug一直在输出,所以——不是程序死掉了,只是显示界面卡死了
为什么界面会卡死呢?
——因为主线程所有的资源都被qDebug()
占用了,无法响应界面事件;
为什么出现return a.exec();
程序不死呢?
——因为它本身是一个事件循环,而UI显示就在该事件循环之中;
为什么嵌入式编程加入while(1)
程序不死呢?
——因为想要运行的函数都放在了while(1)
中了啊。
那对于while(1)
之外的函数呢?
所以说,在这里我采用了一个极端的例子,就是空的死循环,来类比我们平常项目中可能会遇到的一个复杂的函数处理,或者长时间的一个事件响应。如果所有的数据处理都和UI显示放在一个线程中,那么数据处理需要多长时间你的UI界面就会卡死多长时间。
值得注意的是,由于一开始写程序数据量都非常小,一条函数的时间通常在1ms以内,所以界面的卡顿我们根本就察觉不出来;如果数据量非常大到了几百毫秒,那我们拖动窗口就会感觉非常卡了;如果有一些操作用到了while
循环(比如读取或者保存文件时我们会用到while(!file.atend())
)之类的表达,那么注意了,很有可能将出现“程序无法响应”的提示(所以单线程时慎用while
循环)。
所以,这时我们想到了多线程操作——将复杂的、占用线程资源较多的一些函数放到子线程中,可以大大节省主线程资源,提高界面操作与显示的流畅度,也增加了程序运行的可靠性。
3.创建多线程的方法
关于Qt创建多线程的方法,网上有很多介绍,但是对于初学者都不太友好(主要是起的名字太乱、太绕),希望初学者看了我的讲解能够少走弯路,哈哈哈哈先来看一下介绍——
-
Qt4.8之前,创建线程的方法是继承
QThread
类,重写run()
函数。
这是一种直接方法,因为QThread
类为我们提供了大量的线程操作函数,创建也比较简单,一般就分为三步:- 创建继承于
QThread
的线程类; - 重写
run()
函数,将复杂的、耗时较多的函数放入其中; - 调用
start()
函数,启动线程。
又直接又简单那官方后来为什么舍弃这种方法了呢,很重要的一点是
QThread
只有run()
函数是在新线程里的,其他所有函数都在QThread
生成的线程中。话句话说,如果你的Qthread
是UI所在线程生成,那么你在UI线程下调用QThread
的非run()
函数,和在主线程内执行并无区别。 - 创建继承于
-
Qt4.8之后官方建议采用第二种方法,即自定义一个继承于
QObject
的类,然后转移到QThread
中,我们今天也主要讲解这种方法。
二、继承于QObject线程的创建
1.项目创建
创建一个mainwindow
项目,命名为ThreadTest
,结构如下:
接着在项目文件上右击,选择添加新文件——C++ Class,名为myThread
继承于QObject
,如下图所示:
2.代码编写与实现
在mainwindow.h
中添加
#include <QThread>
#include "myThread.h"
在private
中声明两个变量
myThread *myObject;
QThread *newThread;
在这里注意,虽然取名为myThread
,但不要忘了它是继承于QObject
,所以*myObject
只是一个QObject
类的指针,下面的*newThread
才是一个线程指针,一定要分清楚。
接着在主窗口构造函数中将指针例化:
newThread = new QThread;
myObject = new myThread;
这两句话怎么理解呢?
第一句是创建了一个线程容器,它只是一个“容器”,里边目前还是空的;
第二句话创建了一个Qobject
对象,我们想要在子线程中实现的运算和操作都可以在myThread.cpp
中编写,但是呢,它目前还在主线程之中。。。。
所以——
如果把这个QObject
对象移到子线程中,我们的目的不就实现了吗?
确实是这样。Qt提供了一条指令moveToThread(QThread)
可以将一个QObject
类移入目标线程之中,因此我们只需在上面两句话下面加上一句
myObject—>moveToThread(newThread);
,子线程便创建好了。
创建好之后,怎么让它开始与结束呢?
还是以上面的例子为例吧,我们让主线程计时并显示,子线程在while(1)
中不停打印字符串。
先介绍一下会用到的线程类中的函数
start()
线程开始;quit()
线程退出;wait()
等待线程当前操作完成,常和quit()
一起用,即线程完成当前操作后退出。
注意这三个函数都是QThread
中自带的虚函数,直接调用即可,不要我们自己编写。
ui界面如下,两个按钮分别控制子线程的启停,数码管显示定时器计数;
mainwindow.h
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include <QThread>
#include "mythread.h"
#include <QTimer>
#include <QDebug>
namespace Ui {
class MainWindow;
}
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
explicit MainWindow(QWidget *parent = 0);
~MainWindow();
private slots:
void on_pushButton_clicked(); //开始槽函数
void on_pushButton_2_clicked(); //停止槽函数
void timerslot(); //定时器槽函数
private:
myThread *myObject;
QThread *newThread;
QTimer newtimer;
quint8 second = 0; //定时器计数值
Ui::MainWindow *ui;
};
#endif // MAINWINDOW_H
mainwindow.cpp
#include "mainwindow.h"
#include "ui_mainwindow.h"
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow)
{
ui->setupUi(this);
newThread = new QThread;
myObject = new myThread;
myObject->moveToThread(newThread); //创建线程
connect(&newtimer,SIGNAL(timeout()),this,SLOT(timerslot())); //定时器信号与槽
connect(ui->pushButton,SIGNAL(clicked()),myObject,SLOT(display())); //点击按钮1,子线程开始打印程序
newtimer.start(1000);
}
MainWindow::~MainWindow()
{
delete ui;
}
void MainWindow::on_pushButton_clicked()
{
newThread->start();
myObject->setFlag(false); //子线程循环的停止位置false 才能进入循环
}
void MainWindow::timerslot()
{
second++;
ui->lcdNumber->display(QString::number(second)); //数码管显示
newtimer.start();
}
void MainWindow::on_pushButton_2_clicked()
{
myObject->setFlag(true); //子线程循环的停止位置 true 才能退出循环
newThread->quit();
newThread->wait(); //等待线程退出
}
mythread.h
#ifndef MYTHREAD_H
#define MYTHREAD_H
#include <QObject>
class myThread : public QObject
{
Q_OBJECT
public:
explicit myThread(QObject *parent = 0);
bool stopFlag = false;
signals:
public slots:
void setFlag(bool flag = false);
void display();
};
#endif // MYTHREAD_H
mythread.cpp
#include "mythread.h"
#include <QDebug>
myThread::myThread(QObject *parent) : QObject(parent)
{
}
void myThread::display()
{
quint16 a = 0;
while(!stopFlag)
{
qDebug()<<"a = "<<a;
a++;
}
}
void myThread::setFlag(bool flag)
{
stopFlag = flag;
}
程序注释比较详细,请自行阅读,其中有两个地方需要注意:
(1). 想要mythread.cpp
中的函数运行在子线程中,必须采用信号与槽的形式调用,如
connect(ui- >pushButton,SIGNAL(clicked()),myObject,SLOT(display()));
如果是在按钮槽函数中直接调用,如
void MainWindow::on_pushButton_2_clicked()
{
myObject->setFlag(true); //子线程循环的停止位置 true 才能退出循环
newThread->quit();
newThread->wait(); //等待线程退出
}
那么调用的函数还将运行在主线程中.
(2). 为什么while循环要设一个停止标志位
就是因为wait()
的缘故——必须要等到线程当前任务处理完才能完全退出,所以,如果是while(1)
的话,子线程永远退不出来(除非暴力终止)。正因为如此,在停止按钮的槽函数中,首先要将标志位置为true
,然后再发出退出指令;同样,在启动线程时也要先将标志位置false
,然后再发出开始指令。
三、线程间数据的传递
线程的创建与启停已经讲完了,接下来又面临一个问题,就是两个线程不能各干各的吧,既然说子线程用来数据处理,那么处理完的数据该如何返回到主线程呢?这里给大家介绍两种方法
1. 通过外部变量
还是通过例子来说明,这次我们不用定时器计数,而是将while
循环中counter
的值传给主线程进行显示(防止数值增加过快,使用QThread::msleep()
函数稍做延时)。
程序如下:
mainwindow.h
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include <QThread>
#include "mythread.h"
#include <QDebug>
#include <QTimer>
namespace Ui {
class MainWindow;
}
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
explicit MainWindow(QWidget *parent = 0);
~MainWindow();
signals:
private slots:
void on_pushButton_clicked(); //开始槽函数
void on_pushButton_2_clicked(); //停止槽函数
void timerslot(); //定时器槽函数
private:
myThread *myObject;
QThread *newThread;
Ui::MainWindow *ui;
};
#endif // MAINWINDOW_H
mainwindow.cpp
#include "mainwindow.h"
#include "ui_mainwindow.h"
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow)
{
ui->setupUi(this);
newThread = new QThread;
myObject = new myThread;
myObject->moveToThread(newThread); //创建线程
connect(ui->pushButton,SIGNAL(clicked()),myObject,SLOT(display())); //点击按钮1,子线程开始打印程序
connect(myObject,SIGNAL(lcd_show()),this,SLOT(timerslot()));
}
MainWindow::~MainWindow()
{
delete ui;
}
void MainWindow::on_pushButton_clicked()
{
newThread->start();
myObject->setFlag(false); //子线程循环的停止位置false 才能进入循环
}
void MainWindow::timerslot()
{
ui->lcdNumber->display(QString::number(counter)); //数码管显示
}
void MainWindow::on_pushButton_2_clicked()
{
myObject->setFlag(true); //子线程循环的停止位置 true 才能退出循环
newThread->quit();
newThread->wait(); //等待线程退出
}
mythread.h
#ifndef MYTHREAD_H
#define MYTHREAD_H
#include <QObject>
extern int counter;
class myThread : public QObject
{
Q_OBJECT
public:
explicit myThread(QObject *parent = 0);
bool stopFlag = false;
signals:
void lcd_show();
public slots:
void setFlag(bool flag = false);
void display();
};
#endif // MYTHREAD_H
mythread.cpp
#include "mythread.h"
#include <QDebug>
#include <QThread>
int counter = 0;
myThread::myThread(QObject *parent) : QObject(parent)
{
}
void myThread::display()
{
while(!stopFlag)
{
QThread::msleep(100);
counter++;
emit lcd_show();
}
}
void myThread::setFlag(bool flag)
{
stopFlag = flag;
}
因为是mainwindow.h
中引用的mythread.h
,所以在后者中声明一个外部变量
extern int counter;
(注意位置一定要放在类的外面)
然后在对应的cpp文件中定义该变量
int counter = 0;
(一定要注意位置))
这样就可以在包含该头文件的cpp文件中调用该变量了
2.通过信号与槽机制
这种方法实际上在一开始的程序中就已经使用了——
myObject->setFlag(true);
通过信号与槽的机制传递参数思想也是如此,还是通过例子来看一下
在上一个程序中只需要改变lcd_show()
和timerslot()
的定义、调用与connect
函数即可,具体为(改后):
mythread.h
void lcd_show(int a);
mainwindow.h
void timerslot(int a);
mythread.cpp
void myThread::display()
{
while(!stopFlag)
{
QThread::msleep(100);
counter++;
emit lcd_show(counter);
}
}
mainwindow.cpp
connect(myObject,SIGNAL(lcd_show(int)),this,SLOT(timerslot(int)));
void MainWindow::timerslot(int a)
{
ui->lcdNumber->display(QString::number(a)); //数码管显示
}
这里面要注意的是,信号和槽函数的参数类型要严格一致,在写connect
函数时,形参只要表明类型即可,无需加变量名。
以上介绍的两种方法各有利弊,采用外部变量法会破坏函数的封装性,采用信号与槽的方法,在数据类型与数量上也许存在某些限制,在运用时还需根据具体问题具体分析。
四、总结
多线程是我们在使用Qt编程时必须掌握的一个知识点,初学者应先仿照,后创新。本文从多线程的创建与运行到线程间的数据传递都作了较详细的介绍,在实际应用中要解决的问题肯定比本文的举例难得多,在处理具体问题时应先考虑哪些资源要在主线程中,哪些函数要放到子线程中,然后通过线程间的数据传递,实现整体的功能。