欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Qt学习(十七)—— 线程

程序员文章站 2024-03-25 16:07:28
...

为什么要学习线程

假设一个单任务程序中有一段非常复杂的数据处理,需要占用很多内存,就很容易使程序发生卡顿、崩溃的现象。比如,在传输大型文件的过程中,发现进度条停滞了,这时候如果我们不耐烦地多点了几下窗口,就很可能会导致窗口无响应。所以,像这种复杂的数据处理不应该放在界面上,而应该把它放到线程中。

创建一个子线程

在Qt中,主线程工作在用户界面,子线程则是在幕后默默工作。要实现一个子线程,需要新建一个自定义线程类,它继承自QObject,类中实现一个线程处理函数(只有一个是线程处理函数),名字任意。假设我们需要实现一个线程处理函数myTimeout(),在这个函数的功能是每隔1秒钟发送一个信号。
Qt学习(十七)—— 线程
Qt学习(十七)—— 线程

//MyThread.h
#ifndef MYTHREAD_H
#define MYTHREAD_H
#pragma execution_character_set("utf-8")
#include <QObject>

class MyThread : public QObject
{
    Q_OBJECT
public:
    explicit MyThread(QObject *parent = nullptr);
    void MyThread::myTimeout(); //线程函数myTimeout
signals:
    void signalStartTimeout(); //开启Timeout信号

public slots:
};

#endif // MYTHREAD_H

//MyThread.cpp
#include "MyThread.h"
#include <QThread>
#include <QDebug>
MyThread::MyThread(QObject *parent) : QObject(parent)
{

}
void MyThread::myTimeout(){
    while(1){
        QThread::sleep(1);  //设置1s的延时
        emit signalStartTimeout(); //发射信号
    }
}

接着,在主窗口构造函数中创建一个自定义线程对象,且不能指定父对象。再创建一个QThread子线程对象,可以指定父对象。然后再用moveToThread()把自定义线程对象加入到子线程对象中。

//Widget.h
#ifndef WIDGET_H
#define WIDGET_H
#pragma execution_character_set("utf-8")
#include <QWidget>
#include <QLCDNumber>
#include <QPushButton>
#include "MyThread.h"   //引入自定义线程头文件
#include <QThread>
class Widget : public QWidget
{
    Q_OBJECT

public:
    Widget(QWidget *parent = 0);
    ~Widget();
private:
    QLCDNumber *m_pLcdNumber;
    QPushButton *m_pStartButton;
    QPushButton *m_pStopButton;
    MyThread *m_pMyThread;
    QThread *m_pThread;
};

#endif // WIDGET_H

//Widget.cpp
#include "Widget.h"
#include <QHBoxLayout>
#include <QDebug>
Widget::Widget(QWidget *parent)
    : QWidget(parent)
{
    QHBoxLayout *m_pLayout=new QHBoxLayout(this);
    m_pLcdNumber=new QLCDNumber(this);
    m_pStartButton=new QPushButton("start",this);
    m_pStopButton=new QPushButton("stop",this);
    m_pLayout->addWidget(m_pLcdNumber);
    m_pLayout->addWidget(m_pStartButton);
    m_pLayout->addWidget(m_pStopButton);
    //为自定义线程动态分配空间,切记不要指定父对象
    m_pMyThread=new MyThread;
    //创建子线程
    m_pThread=new QThread(this);
    //将自定义线程和子线程进行关联
    m_pMyThread->moveToThread(m_pThread);
}

Widget::~Widget()
{

}

P.S:加入到子线程就好像为这个线程指定了父对象,因此,指定了父对象的线程被移动到子线程时会报错:moveToThread Cannot move object with a parent,所以不能为自定义线程指定父对象。

接下来要实现的是,点击“start”按钮,启动线程并启动线程处理函数

这里需要注意的地方是,启动子线程,只是把子线程开启了,并没有启动线程处理函数。但线程处理函数也不能直接调用,否则会导致线程处理函数和主线程在同一个线程,这样就背离我们使用多线程的初衷了。

//Widget.cpp
#include "Widget.h"
#include <QHBoxLayout>
#include <QDebug>
Widget::Widget(QWidget *parent)
    : QWidget(parent)
{
    QHBoxLayout *m_pLayout=new QHBoxLayout(this);
    m_pLcdNumber=new QLCDNumber(this);
    m_pStartButton=new QPushButton("start",this);
    m_pStopButton=new QPushButton("stop",this);
    m_pLayout->addWidget(m_pLcdNumber);
    m_pLayout->addWidget(m_pStartButton);
    m_pLayout->addWidget(m_pStopButton);
    //为自定义线程动态分配空间,切记不要指定父对象
    m_pMyThread=new MyThread;
    //创建子线程
    m_pThread=new QThread(this);
    //将自定义线程和子线程进行关联
    m_pMyThread->moveToThread(m_pThread);
    //处理start按钮
    connect(m_pStartButton,&QPushButton::clicked,this,&Widget::slotStartThread);
    //处理子进程信号
    connect(m_pMyThread,&MyThread::signalStartTimeout,this,&Widget::slotDealSignal);
    qDebug()<<"主线程为:"<<QThread::currentThread();

}

Widget::~Widget()
{

}
void Widget::slotStartThread(){
    //启动线程
    m_pThread->start();
    //直接调用线程处理函数
    m_pMyThread->myTimeout();	//注意这个地方
}
void Widget::slotDealSignal(){
    static int i=0;
    i++;
    m_pLcdNumber->display(i);
}
//MyThread.cpp
void MyThread::myTimeout(){
    while(1){
        QThread::sleep(1);  //设置1s的延时
        emit signalStartThread(); //发射信号
        qDebug()<<"子线程为:"<<QThread::currentThread();
    }
}

分别在主窗口构造函数(运行在主线程)中和自定义线程处理函数中打印当前的线程号,会发生下面这种情况:Qt学习(十七)—— 线程
说明此时的线程处理函数存在于主线程,而不是子线程。因此,我们应该间接调用线程处理函数,如何间接地调用线程处理函数?需要借助signal和slot

当主线程需要子线程工作的时候,由主线程发送一个信号(signal),主线程自己接收到这个信号之后,调用对应的槽函数(slot)(此时也是自定义线程类中的线程处理函数),这时候再在槽函数中打印当前线程,就会发现和主线程不一样了。

改进后的代码:

//Widget.h
#ifndef WIDGET_H
#define WIDGET_H
#pragma execution_character_set("utf-8")
#include <QWidget>
#include <QLCDNumber>
#include <QPushButton>
#include "MyThread.h"   //引入自定义线程头文件
#include <QThread>
class Widget : public QWidget
{
    Q_OBJECT

public:
    Widget(QWidget *parent = 0);
    ~Widget();
private:
    QLCDNumber *m_pLcdNumber;
    QPushButton *m_pStartButton;
    QPushButton *m_pStopButton;
    MyThread *m_pMyThread;
    QThread *m_pThread;
protected slots:
    void slotStartThread();
    void slotDealSignal();
signals:
    void signalStartThread();	//在主窗口定义一个信号
};

#endif // WIDGET_H
#include "Widget.h"
#include <QHBoxLayout>
#include <QDebug>
Widget::Widget(QWidget *parent)
    : QWidget(parent)
{
    QHBoxLayout *m_pLayout=new QHBoxLayout(this);
    m_pLcdNumber=new QLCDNumber(this);
    m_pStartButton=new QPushButton("start",this);
    m_pStopButton=new QPushButton("stop",this);
    m_pLayout->addWidget(m_pLcdNumber);
    m_pLayout->addWidget(m_pStartButton);
    m_pLayout->addWidget(m_pStopButton);
    //为自定义线程动态分配空间,切记不要指定父对象
    m_pMyThread=new MyThread;
    //创建子线程
    m_pThread=new QThread(this);
    //将自定义线程和子线程进行关联
    m_pMyThread->moveToThread(m_pThread);
    //处理start按钮
    connect(m_pStartButton,&QPushButton::clicked,this,&Widget::slotStartThread);
    //处理子进程信号
    connect(m_pMyThread,&MyThread::signalStartTimeout,this,&Widget::slotDealSignal);
    //启动线程处理函数
    connect(this,&Widget::signalStartThread,m_pMyThread,&MyThread::myTimeout);
    qDebug()<<"主线程为:"<<QThread::currentThread();

}

Widget::~Widget()
{

}
void Widget::slotStartThread(){
    //启动线程
    m_pThread->start();
	//发送信号,启动子线程处理函数
    emit signalStartThread();

}
void Widget::slotDealSignal(){
    static int i=0;
    i++;
    m_pLcdNumber->display(i);
}

实现效果:
Qt学习(十七)—— 线程
现在,把m_pMyThread->moveToThread(m_pThread);注释掉,再次运行程序,看看会发生什么:
Qt学习(十七)—— 线程
把这一句注释掉后,调用线程处理函数的线程又变成主线程了,而且LCD屏上的数字不会改变。这是因为用户界面是运行在主线程的,但是点击“start”按钮后,运行在主线程中的myTimeout()会无线循环sleep()延时函数,导致用户界面崩溃。

说明,关键在于moveToThread(),它的工作就是把线程处理函数放到子线程中去执行,这样即使有非常复杂的数据处理,也不会影响用户界面的正常工作。

关闭子线程

线程工作完之后,如果不关闭,会一直占用线程号,而线程号是有限的,所以应该养成线程中任务执行完毕后就关闭线程的习惯。但是由于线程是工作在后台的,所以即使我们点击程序主窗口右上角的“×”,将用户界面关闭,线程也依然在工作。因此,最好在关闭窗口的同时,把相关的线程也一并关闭。
P.S:关闭窗口时会触发主窗口的destroyed()信号,可以在与之对应的槽函数中关闭线程。关闭线程最好使用quit()方法,而不要使用ternimate()方法,因为后者被调用时,就会立即关闭线程,不管线程是否仍在执行任务,这样就会导致当线程是动态分配的时候产生内存问题。

在加入子线程之后,有时候关闭窗口会看到控制台显示以下信息:
Qt学习(十七)—— 线程
大意是我们在线程仍在运行的时候就把窗口关闭了。因此,如果在关闭窗口时不进行一些处理,就会导致许多线程号在我们关闭窗口后依然被占用。解决这个问题一般需要用到quit()wait()方法,其中,quit()在子进程处理完手头上的工作的时候关闭子进程,wait()用于回收资源。

void Widget::slotStopThread(){
    m_pThread->quit();
    m_pThread->wait();
}

实现效果:
Qt学习(十七)—— 线程
运行程序之后发现,还是存在这个问题。这是因为在这个demo中,事件处理函数是一个死循环,我们需要在按下“stop”按钮时,跳出这个死循环。实现的方法是在自定义线程类中定义一个用来标记循环是否停止的私有数据isStop,并设初始值为false,使其具备进入循环的条件。当“stop”按钮被按下时,在对应的槽函数中修改isStop的值为true,使子进程中的线程处理函数能够跳出循环。

//MyThread.h
#ifndef MYTHREAD_H
#define MYTHREAD_H
#pragma execution_character_set("utf-8")
#include <QObject>

class MyThread : public QObject
{
    Q_OBJECT
public:
    explicit MyThread(QObject *parent = nullptr);
    void myTimeout(); //线程函数myTimeout
    void setStop(bool flag=false);
signals:
    void signalStartTimeout(); //开启Timeout信号
private:
    bool m_isStop;	//标记循环是否停止
public slots:
};

#endif // MYTHREAD_H

//MyThread.cpp
#include "MyThread.h"
#include <QThread>
#include <QDebug>
MyThread::MyThread(QObject *parent) : QObject(parent)
{

}
void MyThread::myTimeout(){
    while(m_isStop==false){
        QThread::sleep(1);  //设置1s的延时
        emit signalStartTimeout(); //发射信号
        qDebug()<<"子线程为:"<<QThread::currentThread();
        if(m_isStop==true){
            qDebug()<<"子线程中isStop的值被改变了,为true";
            break;
        }
    }
}
void MyThread::setStop(bool flag){
    m_isStop=flag;
}

//Widget.cpp
void Widget::slotStopThread(){
    m_pMyThread->setStop(true);
    qDebug()<<"主线程改变了isStop的值,为true";
    m_pThread->quit();
    m_pThread->wait();
}

实现效果:
Qt学习(十七)—— 线程
虽然是在主进程的槽函数中改变了isStop的值,但子进程中依然能实时响应这个改变,并成功跳出循环。

但是现在这个程序明显有些“迟钝”,每次按下“stop”之后,LCD屏还会再多显示一个数字。

不妨来梳理一下主次进程之间的工作流程:
1)主线程:点击“stop”按钮,置isStop的值为true
2)子线程:isStop的值改变为true之前,一定是已经进入某一层循环了。在判断isStop是否为true之前,子线程已经经过1秒钟的延迟,并且还向主线程发送信号,调用主线程中的槽函数,更新LCD屏的读数。

所以,在LCD屏显示数字N的时候按下“stop”键,一定会在它的读数变为N+1时才跳出循环。因此,只要把对isStop的判断放在发送信号之前就可以了。(同时也要放在延迟函数之后,否则跟在while中判断没有区别)

void MyThread::myTimeout(){
    while(m_isStop==false){
        QThread::sleep(1);  //设置1s的延时
        if(m_isStop==true){
            qDebug()<<"子线程中isStop的值被改变了,为true";
            break;
        }
        emit signalStartTimeout(); //发射信号
        qDebug()<<"子线程为:"<<QThread::currentThread();

    }
}

实现效果:
Qt学习(十七)—— 线程
这样看好像是没有问题了,但是每次都要手动点击“stop”按钮才能关闭子线程并回收资源也太麻烦了,如果不这么做,直接关闭窗口,还是会遇到:
Qt学习(十七)—— 线程所以,最好在关闭主窗口时,也就是主窗口的destroyed()信号被触发时,再次调用这个槽函数:

//Widget.cpp
//处理关闭窗口                                                         
connect(this,&Widget::destroyed,this,&Widget::slotDealDestroyed);
//slotDealDestroyed槽函数的实现
void Widget::slotDealDestroyed(){
    slotStopThread();	//可以直接调用slotStopThread(),它的本质也只是一个函数而已
}

实现效果:
Qt学习(十七)—— 线程
这下即使直接关闭主窗口也不会有子线程还在运行的问题了。这个程序还有很多可以优化的地方,比如:
1)在slotStartThread()中,利用子线程的isRunning()方法判断子线程是否正在执行,如果是,就直接return,不再执行后面的启动线程等操作。
2)在slotStopThread()中,利用子线程的isRunning()方法判断子线程是否正在执行,如果否,就直接return,不再执行后面的关闭线程等操作。

void Widget::slotStartThread(){
	//先判断线程是否正在运行
    if(m_pThread->isRunning()){
        return;
    }
    //启动线程
    m_pThread->start();
    m_pMyThread->setStop(false);
    emit signalStartThread();
}

void Widget::slotStopThread(){
	//先判断线程是否正在运行
    if(!m_pThread->isRunning()){
        return;
    }
    m_pMyThread->setStop(true);
    m_pThread->quit();
    m_pThread->wait();
}

使用线程需要注意的地方

  • 线程处理函数内部不允许操作图形界面,它只是用来进行复杂的数据处理的。
  • 自定义线程对象不可以指定父对象,但是这样一来,动态分配的内存没有随着父对象一起被释放,又可能会造成内存泄漏。所以最好在程序关闭时手动用delete释放内存。
  • connect()的第五个参数就是用于多线程的,它指的是连接方式。连接方式主要有三种:默认(自动)、队列、直接
    如果是多线程,默认使用队列连接方式
    如果是单线程,默认使用直接连接方式
    队列连接方式:槽函数所在的线程和接收者所在的线程一样
    直接连接方式:槽函数所在的线程和发送者所在的线程一样
    所以,启动线程处理函数一定要借助信号和槽,才能让它运行在子线程中。

P.S:如有错误,欢迎指正~

相关标签: Qt