文章目录
一、多线程简介
1. 基础知识
2. 多线程的优缺点及注意事项
二、多线程详解
1. 背景案例
2. 通过多线程对背景案例进行优化
3. 方法一:多线程的创建使用(QT 4.7 以前)
3.1 方法一的创建步骤
3.2 方法一的具体实现及实现代码
4. 方法二:多线程的创建使用(QT 4.7 及以后)
4.1 方法二的创建步骤
4.2 方法二的具体实现
4.3 方法二的实现代码
由于每次代码都是在原有程序上修改,因此除了新建项目,不然一般会在学完后统一展示代码。
提示:具体项目创建流程和注意事项见
QT 学习笔记(一)
提示:具体项目准备工作和细节讲解见
QT 学习笔记(二)
一、多线程简介
1. 基础知识
(1) 进程是操作系统结构的基础;是一个正在执行的程序;计算机中正在运行的程序实例;可以分配给处理器并由处理器执行的一个实体;由单一顺序的执行显示,一个当前状态和一组相关的系统资源所描述的活动单元。
(2) 线程是程序中一个单一的顺序控制流程。是程序执行流的最小单元。另外,线程是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源。一个线程可以创建和撤消另一个线程,同一进程中的多个线程之间可以并发执行。由于线程之间的相互制约,致使线程在运行中呈现出间断性。线程也有就绪、阻塞和运行三种基本状态。每一个程序都至少有一个线程,若程序只有一个线程,那就是程序本身。
(3) 多线程是在单个程序中同时运行多个线程完成不同的工作。
2. 多线程的优缺点及注意事项
通常情况下,应用程序都是在一个线程中执行操作。但是,当调用一个耗时操作(例如,大批量 I/O 或大量矩阵变换等 CPU 密集操作)时,用户界面常常会冻结。而使用多线程可以解决这一问题。
多线程程序有以下几个优点:
1) 提高应用程序响应速度。这对于图形界面开发的程序尤为重要,当一个操作耗时很长时,整个系统都会等待这个操作,程序就不能响应键盘、鼠标、菜单等操作,而使用多线程技术可将耗时长的操作置于一个新的线程,避免以上问题。
(2) 使多 CPU 系统更加有效。当前线程数不大于 CPU 数目时,操作系统可以调度不同的线程运行于不同的 CPU 上。
(3) 改善程序结构。一个既长又复杂的进程可以考虑分为多个线程,成为独立或半独立的运行部分,这样有利于代码的理解和维护。
(4) 和进程相比,线程是一种非常花销小,切换快的多任务操作方式。运行于一个进程中的多个线程,它们彼此之间使用相同的地址空间,共享大部分数据,启动一个线程所花费的空间远远小于启动一个进程所花费的空间,而且,线程间彼此切换所需的时间也远远小于进程间切换所需要的时间。
(5) 线程间方便的通信机制。对不同进程来说,它们具有独立的数据空间,要进行数据的传递只能通过通信的方式进行,这种方式不仅费时,而且很不方便。线程则不然,由于同一进程下的线程之间共享数据空间,所以一个线程的数据可以直接为其它线程所用,这不仅快捷,而且方便。
多线程程序有以下几个缺点:
- (1) 多线程程序的行为无法预期,当多次执行程序时,每一次的结果都可能不同。
- (2) 多线程的执行顺序无法保证,它与操作系统的调度策略和线程优先级等因素有关。
- (3) 多线程的切换可能发生在任何时刻、任何地点。
- (4) 多线程对代码的敏感度高,对代码的细微修改都可能产生意想不到的结果。
- 多线程使用过程中注意事项:
- (1) 线程不能操作 ui 对象(从 QWidget 直接或间接派生的窗口对象)
- (2) 需要移动到子线程中处理的模块类,创建的对象的时候不能指定父对象。
二、多线程详解
- 生成一个新的项目,具体步骤过程见提示。
在生成新项目的过程中,我们选择基类为 QWidget ,这是因为 QWidget 当中比较干净,不存在别的东西,而 QMainWindow 虽然也可以实现,但其中存在工具栏,菜单栏,核心控件等东西,比较复杂。
1. 背景案例
在 QT 中使用 QThread 来管理线程。下面来看一个简单的例子:
首先,我们在 ui 界面布置出所需要的窗口界面,包含一个 Display Widgets 当中的 LCD Number,并将其放大,这里需要注意的是,LCD Number 是有范围限定的,是 5 位,超过 5 位的数字就无法显示,一个按钮,用以启动 LCD Number。具体界面布局如下图所示。
在整个过程当中,需要使用到定时器 QTimer。因此,先进行头文件的编写和变量的声明。在实现的过程当中不选择使用 Lambda 表达式,使用传统的槽函数(定时器的槽函数没有参数和返回值)进行功能的实现。
定时器 QTimer 功能完成后,在程序界面对按钮进行转到槽函数操作,并通过按钮完成定时器 QTimer 的启动。
当我们完成代码的编写后,点击启动按钮。此时,ui 界面会无法操作,LCD Number 没有任何变化,具体实现现象如下图所示。
使用 QThread 中的 sleep 函数,让程序等待 5s,我们现在目前只有一个主线程,所以在点击按钮之后会造成定时器虽然设置了,但是 LCD Number 的显示数字是不会改变的,因为 sleep 了 5s 所以说需要等 5s 之后才会开始变化。
如果 sleep 换成一个数据处理的函数时候,在数据处理函数执行的这段时间,其余的程序无法运行,会造成窗口卡住,无响应等问题,影响他人的正常使用。
2. 通过多线程对背景案例进行优化
我们的主界面有一个用于显示时间的 LCD Number 数字面板还有一个用于启动任务的按钮。
程序的目的是用户点击按钮,开始一个非常耗时的运算(程序中我们以程序界面睡眠 5s 来替代这个非常耗时的工作,在真实的程序中,这可能是一个网络访问,可能是需要复制一个很大的文件或者其它任务)。
同时 LCD Number 开始显示逝去的毫秒数。毫秒数通过一个定时器 QTimer 进行更新。计算完成后,计时器停止。
背景案例当中的是一个很简单的应用,也看不出有任何问题。但是当我们开始运行程序时,问题就来了:点击按钮之后,程序界面直接停止响应,直到结束后才开始重新更新。
通过这个问题,我们决定这里使用多线程进行解决。这是因为 QT 中所有界面都是在 ui 线程中(也被称为主线程,就是执行了 QApplication::exec() 的线程),在这个线程中执行耗时的操作(,就会阻塞 ui 线程,从而让界面停止响应。
所以,为了避免这一问题,我们要使用 QThread 开启一个新的线程。
具体多线程的创建使用有如下两种方法。
3. 方法一:多线程的创建使用(QT 4.7 以前)
- 方法一这里直接在背景案例的基础上进行修改。
3.1 方法一的创建步骤
(1) 自定义一个类,继承于 QThread,并且只有一个线程处理函数(和主线程不再同一个线程),这个线程处理函数就是重写父类中的 run 函数。
(2) 线程处理函数里面写入需要执行的复杂数据处理。
(3) 启动线程不能直接调用 run 函数,需要使用对象来调用 start 函数实现线程启动。
(4) 线程处理函数执行结束后可以定义一个信号来告诉主线程。
(5) 最后关闭线程。
3.2 方法一的具体实现及实现代码
- 这里我们对背景案例进行优化,由多线程的创建步骤可知,需要添加一个 C++ 文件和类。
基类选择 QObject
,这里注意千万不要选成 QWidget
,因为我们的线程并不是控件。
在新建完成后,由于我们是要新建一个线程,也就是 QThread。在此,我们对刚刚生成的 QObject 进行修改,将其改为 QThread。
由于线程号是有限的,因此当我们使用完线程号后要及时关闭线程。
在完成上述准备工作和代码编写后,具体实现现象和实现代码如下所示。
(1) 实现现象
按下按钮 start 后,开始计时,至于为什么是在 45 的时候停止的,是由于中间过程启动时间导致的。
在完成计时后,输出 it is over。
- (2) 主窗口头文件 widget.h
#ifndef WIDGET_H #define WIDGET_H #include <QWidget> #include <QTimer> //定时器头文件 #include "mythread.h" //线程头文件 namespace Ui { class Widget; } class Widget : public QWidget { Q_OBJECT public: explicit Widget(QWidget *parent = nullptr); ~Widget(); void dealtimeout(); //定时器槽函数 void dealDone(); //线程结束槽函数 void stopthread(); //停止线程槽函数 private slots: void on_pushButton_clicked(); private: Ui::Widget *ui; QTimer *mytimer; //声明变量 mythread *thread; //线程对象 }; #endif // WIDGET_H
- (3) 主窗口源文件 widget.cpp
#include "widget.h" #include "ui_widget.h" #include <QThread> #include <QDebug> Widget::Widget(QWidget *parent) : QWidget(parent), ui(new Ui::Widget) { ui->setupUi(this); mytimer = new QTimer(this); //只要定时器启动,自动触发timeout信号 connect(mytimer,&QTimer::timeout,this,&Widget::dealtimeout); //分配空间,指定父对象 thread = new mythread(this); connect(thread,&mythread::isDone,this,&Widget::dealDone); //当按窗口右上角关闭按钮X时,窗口触发destroyed()信号 connect(this,&mythread::destroyed,this,&Widget::stopthread); } void Widget::stopthread() { //停止线程 thread->quit(); //等待线程完成当前工作 thread->wait(); } void Widget::dealDone() { qDebug() << "it is over"; mytimer->stop(); //关闭定时器 } void Widget::dealtimeout() { static int i = 0; i++; //设定LCD的值 ui->lcdNumber->display(i); } Widget::~Widget() { delete ui; } void Widget::on_pushButton_clicked() { if(mytimer->isActive() == false) { //如果定时器没有工作 mytimer->start(100); } //启动线程,处理数据 thread->start(); }
- (4) 子线程头文件 mythread.h
#ifndef MYTHREAD_H #define MYTHREAD_H #include <QThread> //自定义一个类重写线程处理函数 class mythread : public QThread { Q_OBJECT public: explicit mythread(QObject *parent = nullptr); protected: //QThread的虚函数 //线程处理函数 //不能直接调用,通过start间接调用 void run(); signals: //自定义一个线程处理函数指向完成后的一个信号 void isDone(); public slots: }; #endif // MYTHREAD_H
- (5) 子线程源文件 mythread.cpp
#include "mythread.h" mythread::mythread(QObject *parent) : QThread(parent) { } void mythread::run() { //非常复杂的数据处理 //需要耗时5s QThread::sleep(5); emit isDone(); }
4. 方法二:多线程的创建使用(QT 4.7 及以后)
- 生成一个新的项目,具体步骤过程见提示。
- 在生成新项目的过程中,我们选择基类为 QWidget ,这是因为 QWidget 当中比较干净,不存在别的东西,而 QMainWindow 虽然也可以实现,但其中存在工具栏,菜单栏,核心控件等东西,比较复杂。
- 此方法比方法一更为复杂一些。
4.1 方法二的创建步骤
1) 自定义一个类,只需要继承 QObject 即可,并且线程处理函数名字随便取,但是也只有一个线程处理函数。
(2) 创建一个自定义线程类的对象,不能指定父对象。
(3) 创建一个 QThread 类的对象,可以指定父对象。
(4) 将自定义线程对象加入到 QThread 类的对象,使用。
(5) 启动线程的时候要注意:启动 QThread 类的对象线程,调用 start 函数只是启动了线程,但是没有开启线程处理函数,线程处理函数的开启需要用到信号槽机制。
(6) 关闭线程。
4.2 方法二的具体实现
其主要特点就是利用 QT 的事件驱动特性,将需要在次线程中处理的业务放在独立的模块(类)中,由主线程创建完该对象后,将其移交给指定的线程,且可以将多个类似的对象移交给同一个线程。
首先,我们在 ui 界面布置出所需要的窗口界面,包含一个 Display Widgets 当中的 LCD Number,并将其放大,两个按钮,用以启动和停止 LCD Number。具体界面布局如下图所示。
这里我们对背景案例进行优化,由多线程的创建步骤可知,需要添加一个 C++ 文件和类。
基类选择 QObject
,这里注意千万不要选成 QWidget
,因为我们的线程并不是控件。
- 在新建完成后,不同于方法一,它本身就是继承父类,因此,不需要将
QObject
修改为QThread
。 - 由于线程号是有限的,因此当我们使用完线程号后要及时关闭线程。
- 如果我们要是有信号和槽,必须有如下图所示的宏。
我们每隔 1s 发送一个信号,通过设置一个 while(1) 死循环,在其中调用 QThread 的 sleep 函数,每睡眠 1s 之后调用信号,如此往复。
然后,在 ui 界面通过按钮 start 进行转到槽操作,启动定时器,由于我们每隔 1s 钟会发送一个信号,因此我们对这些信号进行处理,表明定时器已经正常启动。
这里需要注意的是不能直接调用线程处理函数,直接调用线程处理函数会导致,线程处理函数和主线程在同一个线程。
start() 函数只是启动了线程,但是没有开启线程处理函数,线程处理函数的开启需要用到信号槽机制。
完成启动定时器按钮的编写后,运行程序,点击按钮 start,每隔 1s 定时器计数加一,同时发送一个子线程号,具体实现现象如下图所示。
然后,在 ui 界面通过按钮 close 进行转到槽操作,停止定时器,并回收线程资源(线程号是有限的)。
正常的关闭线程是使用 quit() 函数,但该函数比较温柔,会让线程先完成当前工作再停止,由于我们这里是 while(1) 的死循环,因此,这种关闭线程的方法是不可取的。
完成关闭定时器按钮的编写后,运行程序,当我们点击按钮 close 后,定时器会立刻停止工作,同时子线程号也不再发送,具体实现现象如下图所示。
此时,当我们关闭 ui 界面时,会出现 QThread: Destroyed while thread is still running,表明我们的线程仍在继续工作,因此,我们通过信号和槽函数对这种现象进行修改。
知识点补充:关于 QObject 类的 connect函数第五个参数(只在多线程当中才有意义),连接类型有自动,直接和队列三种。
(1) 自动连接(AutoConnection),默认的连接方式。
如果信号与槽,也就是发送者与接受者在同一线程,等同于直接连接;
如果发送者与接受者处在不同线程,等同于队列连接。
(2) 直接连接(DirectConnection)
当信号发射时,槽函数立即直接调用。
无论槽函数所属对象在哪个线程,槽函数总在发送者所在线程执行。
(3) 队列连接(QueuedConnection)
当控制权回到接受者所在线程的事件循环时,槽函数被调用。槽函数在接受者所在线程执行。
知识点补充:总结如下。
队列连接:槽函数在接受者所在线程执行。
直接连接:槽函数在发送者所在线程执行。
自动连接:二者不在同一线程时,等同于队列连接
4.3 方法二的实现代码
- (1) 主窗口头文件 widget.h
#ifndef WIDGET_H #define WIDGET_H #include <QWidget> #include <QThread> #include "mythread.h" namespace Ui { class Widget; } class Widget : public QWidget { Q_OBJECT public: explicit Widget(QWidget *parent = nullptr); ~Widget(); void dealsignal(); //处理信号槽函数 signals: void startthread(); //启动子线程的信号 void dealclose(); private slots: void on_pushButtonstart_clicked(); void on_pushButton_2_clicked(); private: Ui::Widget *ui; mythread *myt; QThread *thread; }; #endif // WIDGET_H
- (2) 主窗口源文件 widget.cpp
#include "widget.h" #include "ui_widget.h" #include <QDebug> Widget::Widget(QWidget *parent) : QWidget(parent), ui(new Ui::Widget) { ui->setupUi(this); //动态分配空间,不能指定父对象 myt = new mythread; //创建子线程,指定父对象 thread = new QThread(this); //把自定义的线程加入到子线程中 myt->moveToThread(thread); connect(myt,&mythread::mysignal,this,&Widget::dealsignal); qDebug() << "主线程号:" << QThread::currentThread(); connect(this,&Widget::startthread,myt,&mythread::mytimeout); connect(this,&Widget::destroyed,this,&Widget::dealclose); //线程处理函数内部,不允许操作图形界面 } Widget::~Widget() { delete ui; } void Widget::on_pushButtonstart_clicked() { if(thread->isRunning() == true) { return; } //启动线程,但是没有启动线程处理函数 thread->start(); myt->setflag(false); //不能直接调用线程处理函数 //直接调用线程处理函数会导致,线程处理函数和主线程在同一个线程 //只能通过 signal - slot 方式调用 emit startthread(); } void Widget::dealsignal() { static int i = 0; i++; ui->lcdNumber->display(i); } void Widget::on_pushButton_2_clicked() { if(thread->isRunning() == false) { return; } myt->setflag(true); thread->quit(); thread->wait(); } void Widget::dealclose() { myt->setflag(true); thread->quit(); thread->wait(); }
- 3) 子线程头文件 mythread.h
#ifndef MYTHREAD_H #define MYTHREAD_H #include <QObject> class mythread : public QObject { Q_OBJECT public: explicit mythread(QObject *parent = nullptr); //线程处理函数 void mytimeout(); void setflag(bool flag = true); //isstop函数的外部接口 signals: void mysignal(); public slots: private: bool isstop; }; #endif // MYTHREAD_H
- (4) 子线程源文件 mythread.cpp
#include "mythread.h" #include <QThread> #include <QDebug> mythread::mythread(QObject *parent) : QObject(parent) { isstop = false; } void mythread::mytimeout() { while(isstop == false) { QThread::sleep(1); emit mysignal(); qDebug() << "子线程号:" << QThread::currentThread(); if(true == isstop) { break; } } } void mythread::setflag(bool flag) { isstop = flag; }