QT 学习笔记(十七)

简介: QT 学习笔记(十七)

文章目录

一、多线程简介

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。具体界面布局如下图所示。

e3db7ef195c74f1587d6f208a089bb71.png


在整个过程当中,需要使用到定时器 QTimer。因此,先进行头文件的编写和变量的声明。在实现的过程当中不选择使用 Lambda 表达式,使用传统的槽函数(定时器的槽函数没有参数和返回值)进行功能的实现。

定时器 QTimer 功能完成后,在程序界面对按钮进行转到槽函数操作,并通过按钮完成定时器 QTimer 的启动。

当我们完成代码的编写后,点击启动按钮。此时,ui 界面会无法操作,LCD Number 没有任何变化,具体实现现象如下图所示。


91d20a7328104a99b2f8399eaf08f42c.png


使用 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 方法一的创建步骤


b455cf3f9a174794be2343e2c694c344.png


(1) 自定义一个类,继承于 QThread,并且只有一个线程处理函数(和主线程不再同一个线程),这个线程处理函数就是重写父类中的 run 函数。

(2) 线程处理函数里面写入需要执行的复杂数据处理。

(3) 启动线程不能直接调用 run 函数,需要使用对象来调用 start 函数实现线程启动。

(4) 线程处理函数执行结束后可以定义一个信号来告诉主线程。

(5) 最后关闭线程。


3.2 方法一的具体实现及实现代码

  • 这里我们对背景案例进行优化,由多线程的创建步骤可知,需要添加一个 C++ 文件和类。


b7b047d7c1b24f328d4dc1098eb2650d.png


基类选择 QObject,这里注意千万不要选成 QWidget,因为我们的线程并不是控件。

9dfa5b00ef574cc1b426311f4424aaea.png


在新建完成后,由于我们是要新建一个线程,也就是 QThread。在此,我们对刚刚生成的 QObject 进行修改,将其改为 QThread。

由于线程号是有限的,因此当我们使用完线程号后要及时关闭线程。

在完成上述准备工作和代码编写后,具体实现现象和实现代码如下所示。

(1) 实现现象

按下按钮 start 后,开始计时,至于为什么是在 45 的时候停止的,是由于中间过程启动时间导致的。


a149f39df42146fcbd5c40357636dc32.gif


在完成计时后,输出 it is over。

a49b336ec94a4934ad1bc2a808eec02c.png


  • (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 方法二的创建步骤


2446f99a83eb4117a14cb2d57fa1f6b9.png



1) 自定义一个类,只需要继承 QObject 即可,并且线程处理函数名字随便取,但是也只有一个线程处理函数。

(2) 创建一个自定义线程类的对象,不能指定父对象。

(3) 创建一个 QThread 类的对象,可以指定父对象。

(4) 将自定义线程对象加入到 QThread 类的对象,使用。

(5) 启动线程的时候要注意:启动 QThread 类的对象线程,调用 start 函数只是启动了线程,但是没有开启线程处理函数,线程处理函数的开启需要用到信号槽机制。

(6) 关闭线程。


4.2 方法二的具体实现


其主要特点就是利用 QT 的事件驱动特性,将需要在次线程中处理的业务放在独立的模块(类)中,由主线程创建完该对象后,将其移交给指定的线程,且可以将多个类似的对象移交给同一个线程。

首先,我们在 ui 界面布置出所需要的窗口界面,包含一个 Display Widgets 当中的 LCD Number,并将其放大,两个按钮,用以启动和停止 LCD Number。具体界面布局如下图所示。

3d9cf3bdb10a4a9b9ba62f6a413e855c.png


这里我们对背景案例进行优化,由多线程的创建步骤可知,需要添加一个 C++ 文件和类。

fe83bb1023ec4f0ca646ceb45e781301.png



基类选择 QObject,这里注意千万不要选成 QWidget,因为我们的线程并不是控件。


27cdb0ee5aa34298aaeff96bd21c9a98.png


  • 在新建完成后,不同于方法一,它本身就是继承父类,因此,不需要将 QObject 修改为 QThread
  • 由于线程号是有限的,因此当我们使用完线程号后要及时关闭线程。
  • 如果我们要是有信号和槽,必须有如下图所示的宏。

e40b9bb6ca0a4ceda43a8cea29690605.png


我们每隔 1s 发送一个信号,通过设置一个 while(1) 死循环,在其中调用 QThread 的 sleep 函数,每睡眠 1s 之后调用信号,如此往复。

然后,在 ui 界面通过按钮 start 进行转到槽操作,启动定时器,由于我们每隔 1s 钟会发送一个信号,因此我们对这些信号进行处理,表明定时器已经正常启动。

这里需要注意的是不能直接调用线程处理函数,直接调用线程处理函数会导致,线程处理函数和主线程在同一个线程。

start() 函数只是启动了线程,但是没有开启线程处理函数,线程处理函数的开启需要用到信号槽机制。

完成启动定时器按钮的编写后,运行程序,点击按钮 start,每隔 1s 定时器计数加一,同时发送一个子线程号,具体实现现象如下图所示。


fe5386c199954d67a97a1e62f8dd4b78.png


然后,在 ui 界面通过按钮 close 进行转到槽操作,停止定时器,并回收线程资源(线程号是有限的)。

正常的关闭线程是使用 quit() 函数,但该函数比较温柔,会让线程先完成当前工作再停止,由于我们这里是 while(1) 的死循环,因此,这种关闭线程的方法是不可取的。

完成关闭定时器按钮的编写后,运行程序,当我们点击按钮 close 后,定时器会立刻停止工作,同时子线程号也不再发送,具体实现现象如下图所示。40f80b8ec6d4437b8353168d8eb8d064.png


40f80b8ec6d4437b8353168d8eb8d064.png40f80b8ec6d4437b8353168d8eb8d064.png

此时,当我们关闭 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;
}



















相关文章
|
4月前
【Qt 学习笔记】Qt窗口 | 标准对话框 | 消息对话框QMessageBox
【Qt 学习笔记】Qt窗口 | 标准对话框 | 消息对话框QMessageBox
817 4
【Qt 学习笔记】Qt窗口 | 标准对话框 | 消息对话框QMessageBox
|
4月前
|
开发者
【Qt 学习笔记】Qt系统相关 | Qt事件 | 事件的介绍及基本概念
【Qt 学习笔记】Qt系统相关 | Qt事件 | 事件的介绍及基本概念
242 4
|
4月前
【Qt 学习笔记】Qt窗口 | 标准对话框 | 文件对话框QFileDialog
【Qt 学习笔记】Qt窗口 | 标准对话框 | 文件对话框QFileDialog
904 4
|
4月前
|
数据安全/隐私保护
【Qt 学习笔记】Qt窗口 | 对话框 | 模态与非模态对话框的创建
【Qt 学习笔记】Qt窗口 | 对话框 | 模态与非模态对话框的创建
453 4
|
4月前
|
搜索推荐 C++
【Qt 学习笔记】Qt窗口 | 对话框 | 创建自定义对话框
【Qt 学习笔记】Qt窗口 | 对话框 | 创建自定义对话框
168 4
|
4月前
|
API UED
【Qt 学习笔记】Qt窗口 | 状态栏 | QStatusBar的使用及说明
【Qt 学习笔记】Qt窗口 | 状态栏 | QStatusBar的使用及说明
559 4
|
4月前
【Qt 学习笔记】Qt窗口 | 标准对话框 | 输入对话框QInputDialog
【Qt 学习笔记】Qt窗口 | 标准对话框 | 输入对话框QInputDialog
337 3
|
4月前
|
数据可视化
【Qt 学习笔记】Qt窗口 | 标准对话框 | 字体对话框QFontDialog
【Qt 学习笔记】Qt窗口 | 标准对话框 | 字体对话框QFontDialog
112 3
|
4月前
【Qt 学习笔记】Qt窗口 | 标准对话框 | 颜色对话框QColorDialog
【Qt 学习笔记】Qt窗口 | 标准对话框 | 颜色对话框QColorDialog
692 3
|
4月前
【Qt 学习笔记】Qt窗口 | 对话框 | Qt对话框的分类及介绍
【Qt 学习笔记】Qt窗口 | 对话框 | Qt对话框的分类及介绍
168 3