QT学习—使用多线程的两种方法(子类化QThread+子类化QObject)

QT学习—使用多线程的两种方法(子类化QThread+子类化QObject)第一种方法:摘要Qt多线程方法1继承QThread1写一个继承于QThread的线程2QThread的几个函数quitexitterminate函数3正确的终止一个线程4如何正确启动一个线程41正确的启动一个全局线程和UI一直存在的线程42如何启动一个局部线程用完即释放的线程继承QThread的一些总结使用QObject实现多线的方法见:http://blog.csdn.net/czyt…

大家好,又见面了,我是你们的朋友全栈君。

第一种方法:

使用QObject实现多线的方法见:http://blog.csdn.net/czyt1988/article/details/71194457

1.摘要

Qt有两种多线程的方法,其中一种是继承QThread的run函数,另外一种是把一个继承于QObject的类转移到一个Thread里。 
Qt4.8之前都是使用继承QThread的run这种方法,但是Qt4.8之后,Qt官方建议使用第二种方法。两种方法区别不大,用起来都比较方便,但继承QObject的方法更加灵活。这里要记录的是如何正确的创建一个线程,特别是如何正确的退出一个线程。

本文先介绍QThread的普通用法,这个用法可能网上很多文章都介绍过,如果已经了解大可跳过此节,本文重点介绍线程退出的几种方法,根据需求正确的创建和退出线程等问题。

2.Qt多线程方法1 继承QThread

在使用继承QThreadrun方法之前需要了解一条规则:

QThread只有run函数是在新线程里的,其他所有函数都在QThread生成的线程里

QThread只有run函数是在新线程里的 
QThread只有run函数是在新线程里的 
QThread只有run函数是在新线程里的

重要的事情说3遍!!!

,如果QThread是在ui所在的线程里生成,那么QThread的其他非run函数都是和ui线程一样的,所以,QThread的继承类的其他函数尽量别要有太耗时的操作,要确保所有耗时的操作都在run函数里。 
在UI线程下调用QThread的非run函数(其实也不应该直接调用run函数,而应该使用start函数),和执行普通函数无区别,这时,如果这个函数要对QThread的某个变量进行变更,而这个变量在run函数里也会被用到,这时就需要注意加锁的问题,因为可能这个变量前几毫秒刚刚在run中调用,再调用时已经被另外的线程修改了。

2.1写一个继承于QThread的线程

本文的重点不是教会你继承run写一个多线程,任何有编程基础的5分钟就能学会使用QThread的方法,本文真正要讲的是后面那几节,如如何安全的退出一个线程,如何开启一个临时线程,运行结束马上关闭等问题。如果你对QThread有初步了解,那么可以略过这节,但你最好看看这节后面提出的几个问题。

任何继承于QThread的线程都是通过继承QThreadrun函数来实现多线程的,因此,必须重写QThreadrun函数,把复杂逻辑写在QThreadrun函数中。

看看一个普通继承QThread的例子: 
头文件:

#ifndef THREADFROMQTHREAD_H
#define THREADFROMQTHREAD_H
#include <QThread>

class ThreadFromQThread : public QThread
{
    Q_OBJECT
signals:
    void message(const QString& info);
    void progress(int present);
public:
    ThreadFromQThread(QObject* par);
    ~ThreadFromQThread();
    void setSomething();
    void getSomething();
    void setRunCount(int count);
    void run();
    void doSomething();
private:
    int m_runCount;
};

#endif // THREADFROMQTHREAD_H

cpp文件:

#include "ThreadFromQThread.h"
#include <QDebug>
ThreadFromQThread::ThreadFromQThread(QObject* par) : QThread(par)
,m_runCount(20)
{

}

ThreadFromQThread::~ThreadFromQThread()
{
    qDebug() << "ThreadFromQThread::~ThreadFromQThread()";
}

void ThreadFromQThread::setSomething()
{
    msleep(500);
    QString str = QString("%1->%2,thread id:%3").arg(__FUNCTION__).arg(__FILE__).arg((int)QThread::currentThreadId());
    emit message(str);
}

void ThreadFromQThread::getSomething()
{
    msleep(500);
    emit message(QString("%1->%2,thread id:%3").arg(__FUNCTION__).arg(__FILE__).arg((int)QThread::currentThreadId()));
}

void ThreadFromQThread::setRunCount(int count)
{
    m_runCount = count;
    emit message(QString("%1->%2,thread id:%3").arg(__FUNCTION__).arg(__FILE__).arg((int)QThread::currentThreadId()));
}

void ThreadFromQThread::run()
{
    int count = 0;
    QString str = QString("%1->%2,thread id:%3").arg(__FILE__).arg(__FUNCTION__).arg((int)QThread::currentThreadId());
    emit message(str);
    while(1)
    {
        sleep(1);
        ++count;
        emit progress(((float)count / m_runCount) * 100);
        emit message(QString("ThreadFromQThread::run times:%1").arg(count));
        doSomething();
        if(m_runCount == count)
        {
            break;
        }
    }
}

void ThreadFromQThread::doSomething()
{
    msleep(500);
    emit message(QString("%1->%2,thread id:%3").arg(__FUNCTION__).arg(__FILE__).arg((int)QThread::currentThreadId()));    
}

这个简单的例子有一个Qt类常见的内容,包含了普通方法,信号槽,和一个run函数。这里函数setSomething();进行了500ms的延迟,getSomething同理。这是为了验证在QThread::run()之外调用QThread成员函数不会运行在新线程里。

上面代码用到了QThread::currentThreadId()这是一个静态函数,用于返回当前线程句柄,这个值除了区分以外没有别的用处。

为了验证这个线程,编写一个简单的界面,这个界面主要用于验证如下几个问题:、 
– 在UI线程调用setSomething();函数和getSomething();函数会不会卡顿? 
– 在UI线程调用QThread::quit()QThread::exit()函数会不会停止线程? 
– 在UI线程调用QThread::terminate函数会不会停止线程? 
– 如何正确的退出线程?

2.2 QThread的几个函数quit、exit、terminate函数

为了验证上面这些,编写一个简单的界面如下图所示:

这里写图片描述

#include "Widget.h"
#include "ui_Widget.h"
#include "ThreadFromQThread.h"
#include <QDebug>
Widget::Widget(QWidget *parent) :
    QWidget(parent),
    ui(new Ui::Widget)
{
    ui->setupUi(this);
    //控件初始化
    ui->progressBar->setRange(0,100);
    ui->progressBar->setValue(0);
    ui->progressBar_heart->setRange(0,100);
    ui->progressBar_heart->setValue(0);
    //按钮的信号槽关联
    connect(ui->pushButton_qthread1,&QPushButton::clicked
            ,this,&Widget::onButtonQThreadClicked);
    connect(ui->pushButton_qthread1_setSomething,&QPushButton::clicked
            ,this,&Widget::onButtonQthread1SetSomethingClicked);
    connect(ui->pushButton_qthread1_getSomething,&QPushButton::clicked
            ,this,&Widget::onButtonQthread1GetSomethingClicked);
    connect(ui->pushButton_qthreadQuit,&QPushButton::clicked
            ,this,&Widget::onButtonQthreadQuitClicked);
    connect(ui->pushButton_qthreadTerminate,&QPushButton::clicked
            ,this,&Widget::onButtonQthreadTerminateClicked);
    connect(ui->pushButton_qthreadExit,&QPushButton::clicked
            ,this,&Widget::onButtonQThreadExitClicked);
    connect(ui->pushButton_doSomthing,&QPushButton::clicked
            ,this,&Widget::onButtonQThreadDoSomthingClicked);
    connect(ui->pushButton_qthreadRunLocal,&QPushButton::clicked
            ,this,&Widget::onButtonQThreadRunLoaclClicked);
    //
    connect(ui->pushButton_qobjectStart,&QPushButton::clicked
            ,this,&Widget::onButtonObjectMove2ThreadClicked);
    connect(ui->pushButton_objQuit,&QPushButton::clicked
            ,this,&Widget::onButtonObjectQuitClicked);
    //
    connect(&m_heart,&QTimer::timeout,this,&Widget::heartTimeOut);
    m_heart.setInterval(100);
    //全局线程的创建
    m_thread = new ThreadFromQThread(this);
    connect(m_thread,&ThreadFromQThread::message
            ,this,&Widget::receiveMessage);
    connect(m_thread,&ThreadFromQThread::progress
            ,this,&Widget::progress);
    connect(m_thread,&QThread::finished
            ,this,&Widget::onQThreadFinished);

    m_heart.start();
}



Widget::~Widget()
{
    qDebug() << "start destroy widget";
    m_thread->stopImmediately();//由于此线程的父对象是Widget,因此退出时需要进行判断
    m_thread->wait();
    delete ui;
    qDebug() << "end destroy widget";
}

void Widget::onButtonQThreadClicked()
{
    ui->progressBar->setValue(0);
    if(m_thread->isRunning())
    {
        return;
    }
    m_thread->start();
}

void Widget::progress(int val)
{
    ui->progressBar->setValue(val);
}

void Widget::receiveMessage(const QString &str)
{
    ui->textBrowser->append(str);
}

void Widget::heartTimeOut()
{
    static int s_heartCount = 0;
    ++s_heartCount;
    if(s_heartCount > 100)
    {
        s_heartCount = 0;
    }
    ui->progressBar_heart->setValue(s_heartCount);
}

void Widget::onButtonQthread1SetSomethingClicked()
{
    m_thread->setSomething();
}

void Widget::onButtonQthread1GetSomethingClicked()
{
    m_thread->getSomething();
}

void Widget::onButtonQthreadQuitClicked()
{
    ui->textBrowser->append("m_thread->quit() but not work");
    m_thread->quit();
}

void Widget::onButtonQthreadTerminateClicked()
{
    m_thread->terminate();
}

void Widget::onButtonQThreadDoSomthingClicked()
{
    m_thread->doSomething();
}

void Widget::onButtonQThreadExitClicked()
{
    m_thread->exit();
}

void Widget::onQThreadFinished()
{
    ui->textBrowser->append("ThreadFromQThread finish");
}

界面为上面提到的几个问题提供了按钮, 界面有一个心跳进度条,它是主程序的定时器控制,每100ms触发用于证明主程序的ui线程没有卡死。第二个进度条由线程控制。

点击”QThread run”按钮,触发onButtonQThreadClicked槽,子线程会运行,子线程运行起来后,会打印

../QtThreadTest/ThreadFromQThread.cpp->run,thread id:2900388672

可以确定线程运行的id是2900388672 
子线程是个循环,每次循环都会有打印信息:

ThreadFromQThread::run times:1 
doSomething->../QtThreadTest/ThreadFromQThread.cpp,thread id:2900388672 
ThreadFromQThread::run times:2 
doSomething->../QtThreadTest/ThreadFromQThread.cpp,thread id:2900388672

doSomething是在run函数里调用,其线程id是2900388672,可见这时doSomething函数是运行在子线程里的。

这时,我在界面点击getSomething,setSomething,doSomething会打印:

getSomething->../QtThreadTest/ThreadFromQThread.cpp,thread id:3021526784 
setSomething->../QtThreadTest/ThreadFromQThread.cpp,thread id:3021526784 
doSomething->../QtThreadTest/ThreadFromQThread.cpp,thread id:3021526784

说明在非run函数里调用QThread的成员函数,并不是在线程里运行(3021526784是widget所在线程)

这时我点击quit,thread并没进行任何处理,QThread在不调用exec()情况下是exit函数和quit函数是没有作用的。

m_thread->quit() but not work

点击terminate按钮,线程马上终止,打印:

ThreadFromQThread finish

动态图如下图所示:

因此可以看出quitexit函数都不会中途终端线程,要马上终止一个线程可以使用terminate函数,但这个函数存在非常不安定因素,不推荐使用。那么如何安全的终止一个线程呢?

2.3 正确的终止一个线程

最简单的方法是添加一个bool变量,通过主线程修改这个bool变量来进行终止,但这样有可能引起访问冲突,需要加锁 
我们需要在原来的头文件加上如下语句:

#include <QMutex>

class ThreadFromQThread : public QThread
{
...........
public slots:
    void stopImmediately();
private:
    QMutex m_lock;
    bool m_isCanRun;
.........
};

run函数需要进行修改:

void ThreadFromQThread::stopImmediately()
{
    QMutexLocker locker(&m_lock);
    m_isCanRun = false;
}

void ThreadFromQThread::run()
{
    int count = 0;
    m_isCanRun = true;//标记可以运行
    QString str = QString("%1->%2,thread id:%3").arg(__FILE__).arg(__FUNCTION__).arg((unsigned int)QThread::currentThreadId());
    emit message(str);
    while(1)
    {
        sleep(1);
        ++count;
        emit progress(((float)count / m_runCount) * 100);
        emit message(QString("ThreadFromQThread::run times:%1").arg(count));
        doSomething();
        if(m_runCount == count)
        {
            break;
        }

        {
            QMutexLocker locker(&m_lock);
            if(!m_isCanRun)//在每次循环判断是否可以运行,如果不行就退出循环
            {
                return;
            }
        }
    }
}

QMutexLocker可以安全的使用QMutex,以免忘记解锁(有点类似std::unique_ptr),这样每次循环都会看看是否要马上终止。 
在线程需要马上退出时,可以在外部调用stopImmediately()函数终止线程,之前的例子可以知道,由于在主线程调用QThread非run()函数的函数都是在主线程运行,因此,在主线程调用类似m_thread->stopImmediately()会几乎马上把线程的成员变量m_isCanRun设置为false(面对多线程问题要用面向过程的思维思考),因此在子线程的run函数的循环中遇到m_isCanRun的判断后就会退出run函数,继承QThread的函数在运行完run函数后就视为线程完成,会发射finish信号。

2.4 如何正确启动一个线程

线程的启动有几种方法,这几种方法设计到它的父对象归属问题,和如何删除他的问题。首先要搞清楚这个线程是否和UI的生命周期一致,直到UI结束线程才结束,还是这个线程只是临时生成,等计算完成就销毁。

第一种情况的线程在创建时会把生成线程的窗体作为它的父对象,这样窗体结束时会自动析构线程的对象。但这时候要注意一个问题,就是窗体结束时线程还未结束如何处理,如果没有处理这种问题,你会发现关闭窗口时会导致程序崩溃。往往这种线程是一个监控线程,如监控某个端口的线程。为了好区分,暂时叫这种叫全局线程,它在UI的生命周期中都存在。

第二种情况是一种临时线程,这种线程一般是突然要处理一个大计算,为了不让UI假死需要触发的线程,这时需要注意一个问题,就是在线程还没计算完成,用户突然终止或变更时如何处理,这种线程往往更多见且更容易出错,如打开一个大文件,显示一个大图片,用户可能看一个大图片还没等图片处理完成又切换到下一个图片,这时绘图线程要如何处理才能顺利解决?为了好区分,暂时叫这种叫局部线程,它在UI的生命周期中仅仅是某时刻才会触发,然后销毁。

这就涉及到如何终止正在执行的线程这个问题!

2.4.1正确的启动一个全局线程(和UI一直存在的线程)

我发现大部分网上的教程都是教你创建一个全局的线程,但往往这种线程用的不多,也比较好管理,需要注意的是程序退出时对线程的处理问题。 
在ui的头文件中声明一个线程的指针

widget.h:

ThreadFromQThread* m_thread;

wodget.cpp:

class Widget : public QWidget
{
    Q_OBJECT

public:
    explicit Widget(QWidget *parent = 0);
    ~Widget();
private slots:
    void onButtonQThreadClicked();
    void onButtonQthread1SetSomethingClicked();
    void onButtonQthread1GetSomethingClicked();
    void onButtonQthreadQuitClicked();
    void onButtonQthreadTerminateClicked();
    void onButtonQThreadDoSomthingClicked();
    void onQThreadFinished();
......
    void progress(int val);
    void receiveMessage(const QString& str);
    void heartTimeOut();
private:
    Ui::Widget *ui;
    ThreadFromQThread* m_thread;
    QTimer m_heart;
......
};

先看窗体生成的构造函数

Widget::Widget(QWidget *parent) :
    QWidget(parent),
    ui(new Ui::Widget)
  ,m_objThread(NULL)
{

   ui->setupUi(this);
    //控件初始化
    ui->progressBar->setRange(0,100);
    ui->progressBar->setValue(0);
    ui->progressBar_heart->setRange(0,100);
    ui->progressBar_heart->setValue(0);
    //按钮的信号槽关联
    connect(ui->pushButton_qthread1,&QPushButton::clicked
            ,this,&Widget::onButtonQThreadClicked);
    connect(ui->pushButton_qthread1_setSomething,&QPushButton::clicked
            ,this,&Widget::onButtonQthread1SetSomethingClicked);
    connect(ui->pushButton_qthread1_getSomething,&QPushButton::clicked
            ,this,&Widget::onButtonQthread1GetSomethingClicked);
    connect(ui->pushButton_qthreadQuit,&QPushButton::clicked
            ,this,&Widget::onButtonQthreadQuitClicked);
    connect(ui->pushButton_qthreadTerminate,&QPushButton::clicked
            ,this,&Widget::onButtonQthreadTerminateClicked);
    connect(ui->pushButton_doSomthing,&QPushButton::clicked
            ,this,&Widget::onButtonQThreadDoSomthingClicked);
    //心跳的关联
    connect(&m_heart,&QTimer::timeout,this,&Widget::heartTimeOut);
    m_heart.setInterval(100);
    //全局线程的创建
    //全局线程创建时可以把窗体指针作为父对象
    m_thread = new ThreadFromQThread(this);
    //关联线程的信号和槽
    connect(m_thread,&ThreadFromQThread::message
            ,this,&Widget::receiveMessage);//
    connect(m_thread,&ThreadFromQThread::progress
            ,this,&Widget::progress);
    connect(m_thread,&QThread::finished
            ,this,&Widget::onQThreadFinished);
    //UI心跳开始
    m_heart.start();
}

由于是全局存在的线程,因此在窗体创建时就创建线程,可以把线程的父对象设置为窗体,这时需要注意,别手动delete线程指针。用于你的QThread是在Qt的事件循环里面,手动delete会发生不可预料的意外。理论上所有QObject都不应该手动delete,如果没有多线程,手动delete可能不会发生问题,但是多线程情况下delete非常容易出问题,那是因为有可能你要删除的这个对象在Qt的事件循环里还排队,但你却已经在外面删除了它,这样程序会发生崩溃

如果你确实要删除,请参阅void QObject::deleteLater () [slot]这个槽,这个槽非常有用,尤其是对局部线程来说。后面会经常用到它用于安全的结束线程。

在需要启动线程的地方调用start函数即可启动线程。

void Widget::onButtonQThreadClicked()
{
    ui->progressBar->setValue(0);
    if(m_thread->isRunning())
    {
        return;
    }
    m_thread->start();
}

如果线程已经运行,你重复调用start其实是不会进行任何处理。

一个全局线程就那么简单,要用的时候start一下就行。真正要注意的是如何在ui结束时把线程安全退出。

在widget的析构函数应该这样写:

Widget::~Widget()
{
    qDebug() << "start destroy widget";
    m_thread->stopImmediately();
    m_thread->wait();
    delete ui;
    qDebug() << "end destroy widget";
}

这里要注意的是m_thread->wait();这一句,这一句是主线程等待子线程结束才能继续往下执行,这样能确保过程是单一往下进行的,也就是不会说子线程还没结束完,主线程就destrioy掉了(m_thread的父类是主线程窗口,主线程窗口如果没等子线程结束就destroy的话,会顺手把m_threaddelete这时就会奔溃了),因此wait的作用就是挂起,一直等到子线程结束。

还有一种方法是让QThread自己删除自己,就是在new线程时,不指定父对象,通过绑定void QObject::deleteLater () [slot]槽让它自动释放。这样在widget析构时可以免去m_thread->wait();这句。

2.4.2 如何启动一个局部线程(用完即释放的线程)

启动一个局部线程(就是运行完自动删除的线程)方法和启动全局线程差不多,但要关联多一个槽函数,就是之前提到的void QObject::deleteLater () [slot],这个槽函数是能安全释放线程资源的关键(直接delete thread指针不安全)。

简单的例子如下:

void Widget::onButtonQThreadRunLoaclClicked()
{
    //局部线程的创建的创建
    ThreadFromQThread* thread = new ThreadFromQThread(NULL);//这里父对象指定为NULL
    connect(thread,&ThreadFromQThread::message
            ,this,&Widget::receiveMessage);
    connect(thread,&ThreadFromQThread::progress
            ,this,&Widget::progress);
    connect(thread,&QThread::finished
            ,this,&Widget::onQThreadFinished);
    connect(thread,&QThread::finished
            ,thread,&QObject::deleteLater);//线程结束后调用deleteLater来销毁分配的内存
    thread->start();
}

这个例子还是启动之前的线程,但不同的是: 
– new ThreadFromQThread(NULL);并没有给他指定父对象 
– connect(thread,&QThread::finished ,thread,&QObject::deleteLater);线程结束后调用deleteLater来销毁分配的内存。 
再线程运行完成,发射finished信号后会调用deleteLater函数,在确认消息循环中没有这个线程的对象后会销毁。

但是要注意避免重复点按钮重复调用线程的情况,对于一些需求,线程开启后再点击按钮不会再重新生成线程,一直等到当前线程执行完才能再次点击按钮,这种情况很好处理,加个标记就可以实现,也一般比较少用。

另外更多见的需求是,再次点击按钮,需要终结上次未执行完的线程,重新执行一个新线程。这种情况非常多见,例如一个普通的图片浏览器,都会有下一张图和上一张图这种按钮,浏览器加载图片一般都在线程里执行(否则点击超大图片时图片浏览器会类似卡死的状态),用户点击下一张图片时需要终止正在加载的当前图片,加载下一张图片。你不能要求客户要当前图片加载完才能加载下一张图片,这就几乎沦为单线程了。这时候,就需要终止当前线程,开辟新线程加载下一个图片。

这时,上面的函数将会是大概这个样子的

UI的头文件需要一个成员变量记录正在运行的线程

private slots:
   void onLocalThreadDestroy(QObject* obj);
private:
   QThread* m_currentRunLoaclThread;

运行生成临时线程的函数将变为

void Widget::onButtonQThreadRunLoaclClicked()
{
    //局部线程的创建的创建
    if(m_currentRunLoaclThread)
    {
         m_currentRunLoaclThread->stopImmediately();
    }
    ThreadFromQThread* thread = new ThreadFromQThread(NULL);
    connect(thread,&ThreadFromQThread::message
            ,this,&Widget::receiveMessage);
    connect(thread,&ThreadFromQThread::progress
            ,this,&Widget::progress);
    connect(thread,&QThread::finished
            ,this,&Widget::onQThreadFinished);
    connect(thread,&QThread::finished
            ,thread,&QObject::deleteLater);//线程结束后调用deleteLater来销毁分配的内存
    connect(thread,&QObject::destroyed,this,&Widget::onLocalThreadDestroy);
    thread->start();
    m_currentRunLoaclThread = thread;
}

void Widget::onLocalThreadDestroy(QObject *obj)
{
    if(qobject_cast<QObject*>(m_currentRunLoaclThread) == obj)
    {
        m_currentRunLoaclThread = NULL;
    }
}

这里一个临时变量记录当前正在运行的局部线程,由于线程结束时会销毁自己,因此要通知主线程把这个保存线程指针的临时变量设置为NULL 
因此用到了QObject::destroyed信号,在线程对象析构时通知UI把m_currentRunLoaclThread设置为nullptr;

3. 继承QThread的一些总结

  • QThread只有run函数是在新线程里的

  • QThread执行start函数之后,run函数还未运行完毕,再次start会出现什么后果?

答案是:不会发生任何结果,QThread还是继续执行它的run函数,run函数不会被重新调用。虽然在线程未结束时调用start不会出现什么结果,但为了谨慎起见,还是建议在start之前进行判断:

void Widget::onButtonQThreadClicked()
{
    ui->progressBar->setValue(0);
    if(m_thread->isRunning())
    {
        return;
    }
    m_thread->start();
}

这种调用方法估计了解过QThread的都知道

  • 在线程运行过程调用quit函数有什么效果

答案是:不会发生任何效果,QThread不会因为你调用quit函数而退出正在运行到一半的run,正确退出线程的方法上面有介绍。那quit到底有什么用的呢,这要到下篇才能看出它的作用。使用moveToThread方法执行多线程时,这个函数将有大作用。

  • 程序在退出时要判断各线程是否已经退出,没退出的应该让它终止 
    如果不进行判断,很可能程序退出时会崩溃。如果线程的父对象是窗口对象,那么在窗体的析构函数中,还需要调用wait函数等待线程完全结束再进行下面的析构。

  • 善用QObject::deleteLater 和 QObject::destroyed来进行内存管理 
    由于多线程环境你不可预料下一步是哪个语句执行,因此,加锁和自动删除是很有用的工具,加锁是通过效率换取安全,用Qt的信号槽系统可以更有效的处理这些问题。

//

第二种方法:

现在Qt官方并不是很推荐继承QThread来实现多线程方法,而是极力推崇继承QObject的方法来实现,当然用哪个方法实现要视情况而定,别弄错了就行,估计Qt如此推崇继承QObject的方法可能是QThread太容易用错的原因。

继承QThread实现多线程的方法点此

1. 前言

上一篇介绍了传统的多线程使用方法——继承QThread来实现多线程,这也是很多框架的做法(MFC),但Qt还有一种多线程的实现方法,比直接继承QThread更为灵活,就是直接继承QObject实现多线程。

QObject是Qt框架的基本类,但凡涉及到信号槽有关的类都是继承于QObjectQObject是一个功能异常强大的类,它提供了Qt关键技术信号和槽的支持以及事件系统的支持,同时它提供了线程操作的接口,也就是QObject是可以选择不同的线程里执行的。

QObject的线程转移函数是:void moveToThread(QThread * targetThread) ,通过此函数可以把一个顶层Object(就是没有父级)转移到一个新的线程里。

QThread非常容易被新手误用,主要是QThread自身并不生存在它run函数所在的线程,而是生存在旧的线程中,此问题在上一篇重点描述了。由于QThread的这个特性,导致在调用QThread的非run函数容易在旧线程中执行,因此人们发现了一个新的魔改QThread的方法: 
人们发现,咦,QThread也继承QObjectQObject有个函数void moveToThread(QThread * targetThread)可以把Object的运行线程转移,那么:(下面是非常不推荐的魔改做法,别用此方法):


  1. class MyThread : public QThread{
  2. public:
  3. MyThread ()
  4. {
  5. moveToThread(this);
  6. }
  7. ……
  8. };

直接把MyThread整个转移到MyThread的新线程中,MyThread不仅run,其它函数也在新线程里了。这样的确可以运行正常,但这并不是QThread设计的初衷,Qt还专门发过一篇文章来吐槽这个做法。

在Qt4.8之后,Qt多线程的写法最好还是通过QObject来实现,和线程的交互通过信号和槽(实际上其实是通过事件)联系。

2.继承QObject的多线程实现

QObject来实现多线程有个非常好的优点,就是默认就支持事件循环(Qt的许多非GUI类也需要事件循环支持,如QTimerQTcpSocket),QThread要支持事件循环需要在QThread::run()中调用QThread::exec()来提供对消息循环的支持,否则那些需要事件循环支持的类都不能正常发送信号,因此如果要使用信号和槽,那就直接使用QObject来实现多线程。

2.1 创建及销毁线程

继承QObject多线程的方法线程的创建很简单,只要让QThreadstart函数运行起来就行,但是需要注意销毁线程的方法 在线程创建之后,这个QObject的销毁不应该在主线程里进行,而是通过deleteLater槽进行安全的销毁,因此,继承QObject多线程的方法在创建时有几个槽函数需要特别关注:

  • 一个是QThreadfinished信号对接QObjectdeleteLater使得线程结束后,继承QObject的那个多线程类会自己销毁
  • 另一个是QThreadfinished信号对接QThread自己的deleteLater,这个不是必须,下面官方例子就没这样做

看看Qt官方文档的例子:


  1. class Worker : public QObject
  2. {
  3. Q_OBJECT
  4. public slots:
  5. void doWork(const QString &parameter) {
  6. QString result;
  7. /* ... here is the expensive or blocking operation ... */
  8. emit resultReady(result);
  9. }
  10. signals:
  11. void resultReady(const QString &result);
  12. };
  13. class Controller : public QObject
  14. {
  15. Q_OBJECT
  16. QThread workerThread;
  17. public:
  18. Controller() {
  19. Worker *worker = new Worker;
  20. worker->moveToThread(&workerThread);
  21. connect(&workerThread, &QThread::finished, worker, &QObject::deleteLater);
  22. connect(this, &Controller::operate, worker, &Worker::doWork);
  23. connect(worker, &Worker::resultReady, this, &Controller::handleResults);
  24. workerThread.start();
  25. }
  26. ~Controller() {
  27. workerThread.quit();
  28. workerThread.wait();
  29. }
  30. public slots:
  31. void handleResults(const QString &);
  32. signals:
  33. void operate(const QString &);
  34. };

使用QObject创建多线程的方法如下:

  • 写一个继承QObject的类,对需要进行复杂耗时逻辑的入口函数声明为槽函数
  • 此类在旧线程new出来,不能给它设置任何父对象
  • 同时声明一个QThread对象,在官方例子里,QThread并没有new出来,这样在析构时就需要调用QThread::wait(),如果是堆分配的话, 可以通过deleteLater来让线程自杀
  • 把obj通过moveToThread方法转移到新线程中,此时object已经是在线程中了
  • 把线程的finished信号和object的deleteLater槽连接,这个信号槽必须连接,否则会内存泄漏
  • 正常连接其他信号和槽(在连接信号槽之前调用moveToThread,不需要处理connect的第五个参数,否则就显示声明用Qt::QueuedConnection来连接
  • 初始化完后调用’QThread::start()’来启动线程
  • 在逻辑结束后,调用QThread::quit退出线程的事件循环

使用QObject来实现多线程比用继承QThread的方法更加灵活,整个类都是在新的线程中,通过信号槽和主线程传递数据,前篇文章的例子用继承QObject的方法实现的话,代码如下: 
头文件(ThreadObject.h):


  1. #include <QObject>
  2. #include <QMutex>
  3. class ThreadObject : public QObject
  4. {
  5. Q_OBJECT
  6. public:
  7. ThreadObject(QObject* parent = NULL);
  8. ~ThreadObject();
  9. void setRunCount(int count);
  10. void stop();
  11. signals:
  12. void message(const QString& info);
  13. void progress(int present);
  14. public slots:
  15. void runSomeBigWork1();
  16. void runSomeBigWork2();
  17. private:
  18. int m_runCount;
  19. int m_runCount2;
  20. bool m_isStop;
  21. QMutex m_stopMutex;
  22. };

cpp文件(ThreadObject.cpp):


  1. #include "ThreadObject.h"
  2. #include <QThread>
  3. #include <QDebug>
  4. #include <QMutexLocker>
  5. #include <QElapsedTimer>
  6. #include <limits>
  7. ThreadObject::ThreadObject(QObject *parent):QObject(parent)
  8. ,m_runCount(10)
  9. ,m_runCount2(std::numeric_limits<int>::max())
  10. ,m_isStop(true)
  11. {
  12. }
  13. ThreadObject::~ThreadObject()
  14. {
  15. qDebug() << "ThreadObject destroy";
  16. emit message(QString("Destroy %1->%2,thread id:%3").arg(__FUNCTION__).arg(__FILE__).arg((int)QThread::currentThreadId()));
  17. }
  18. void ThreadObject::setRunCount(int count)
  19. {
  20. m_runCount = count;
  21. emit message(QString("%1->%2,thread id:%3").arg(__FUNCTION__).arg(__FILE__).arg((int)QThread::currentThreadId()));
  22. }
  23. void ThreadObject::runSomeBigWork1()
  24. {
  25. {
  26. QMutexLocker locker(&m_stopMutex);
  27. m_isStop = false;
  28. }
  29. int count = 0;
  30. QString str = QString("%1->%2,thread id:%3").arg(__FILE__).arg(__FUNCTION__).arg((int)QThread::currentThreadId());
  31. emit message(str);
  32. int process = 0;
  33. while(1)
  34. {
  35. {
  36. QMutexLocker locker(&m_stopMutex);
  37. if(m_isStop)
  38. return;
  39. }
  40. if(m_runCount == count)
  41. {
  42. break;
  43. }
  44. sleep(1);
  45. int pro = ((float)count / m_runCount) * 100;
  46. if(pro != process)
  47. {
  48. process = pro;
  49. emit progress(((float)count / m_runCount) * 100);
  50. emit message(QString("Object::run times:%1,m_runCount:%2").arg(count).arg(m_runCount2));
  51. }
  52. ++count;
  53. }
  54. }
  55. void ThreadObject::runSomeBigWork2()
  56. {
  57. {
  58. QMutexLocker locker(&m_stopMutex);
  59. m_isStop = false;
  60. }
  61. int count = 0;
  62. QString str = QString("%1->%2,thread id:%3").arg(__FILE__).arg(__FUNCTION__).arg((int)QThread::currentThreadId());
  63. emit message(str);
  64. int process = 0;
  65. QElapsedTimer timer;
  66. timer.start();
  67. while(1)
  68. {
  69. {
  70. QMutexLocker locker(&m_stopMutex);
  71. if(m_isStop)
  72. return;
  73. }
  74. if(m_runCount2 == count)
  75. {
  76. break;
  77. }
  78. int pro = ((float)count / m_runCount2) * 100;
  79. if(pro != process)
  80. {
  81. process = pro;
  82. emit progress(pro);
  83. emit message(QString("%1,%2,%3,%4")
  84. .arg(count)
  85. .arg(m_runCount2)
  86. .arg(pro)
  87. .arg(timer.elapsed()));
  88. timer.restart();
  89. }
  90. ++count;
  91. }
  92. }
  93. void ThreadObject::stop()
  94. {
  95. QMutexLocker locker(&m_stopMutex);
  96. emit message(QString("%1->%2,thread id:%3").arg(__FUNCTION__).arg(__FILE__).arg((int)QThread::currentThreadId()));
  97. m_isStop = true;
  98. }

这个Object有两个耗时函数work1和work2,这两个耗时函数的调用都是通过槽函数触发,同时为了能及时打断线程,添加了一个stop函数,stop函数不是通过信号槽触发,因此需要对数据进行保护,这里用了互斥锁对一个bool变量进行了保护处理,当然会失去一些性能。

主界面的头文件(截取部分代码):


  1. #include <QWidget>
  2. #include <QTimer>
  3. class ThreadFromQThread;
  4. class ThreadObject;
  5. namespace Ui {
  6. class Widget;
  7. }
  8. class Widget : public QWidget
  9. {
  10. Q_OBJECT
  11. public:
  12. explicit Widget(QWidget *parent = 0);
  13. ~Widget();
  14. signals:
  15. void startObjThreadWork1();
  16. void startObjThreadWork2();
  17. private slots:
  18. ……
  19. void onButtonObjectMove2ThreadClicked();
  20. void onButtonObjectMove2Thread2Clicked();
  21. void onButtonObjectQuitClicked();
  22. void onButtonObjectThreadStopClicked();
  23. void progress(int val);
  24. void receiveMessage(const QString& str);
  25. void heartTimeOut();
  26. private
  27. void startObjThread();
  28. private:
  29. Ui::Widget *ui;
  30. ……
  31. ThreadObject* m_obj;
  32. QThread* m_objThread;
  33. };

cpp文件


  1. Widget::~Widget()
  2. {
  3. qDebug() << "start destroy widget";
  4. if(m_objThread)
  5. {
  6. m_objThread->quit();
  7. }
  8. m_objThread->wait();
  9. qDebug() << "end destroy widget";
  10. }
  11. //创建线程
  12. void Widget::startObjThread()
  13. {
  14. if(m_objThread)
  15. {
  16. return;
  17. }
  18. m_objThread= new QThread();
  19. m_obj = new ThreadObject();
  20. m_obj->moveToThread(m_objThread);
  21. connect(m_objThread,&QThread::finished,m_objThread,&QObject::deleteLater);
  22. connect(m_objThread,&QThread::finished,m_obj,&QObject::deleteLater);
  23. connect(this,&Widget::startObjThreadWork1,m_obj,&ThreadObject::runSomeBigWork1);
  24. connect(this,&Widget::startObjThreadWork2,m_obj,&ThreadObject::runSomeBigWork2);
  25. connect(m_obj,&ThreadObject::progress,this,&Widget::progress);
  26. connect(m_obj,&ThreadObject::message,this,&Widget::receiveMessage);
  27. m_objThread->start();
  28. }
  29. //调用线程的runSomeBigWork1
  30. void Widget::onButtonObjectMove2ThreadClicked()
  31. {
  32. if(!m_objThread)
  33. {
  34. startObjThread();
  35. }
  36. emit startObjThreadWork1();//主线程通过信号换起子线程的槽函数
  37. ui->textBrowser->append("start Obj Thread work 1");
  38. }
  39. //调用线程的runSomeBigWork2
  40. void Widget::onButtonObjectMove2Thread2Clicked()
  41. {
  42. if(!m_objThread)
  43. {
  44. startObjThread();
  45. }
  46. emit startObjThreadWork2();//主线程通过信号换起子线程的槽函数
  47. ui->textBrowser->append("start Obj Thread work 2");
  48. }
  49. //调用线程的中断
  50. void Widget::onButtonObjectThreadStopClicked()
  51. {
  52. if(m_objThread)
  53. {
  54. if(m_obj)
  55. {
  56. m_obj->stop();
  57. }
  58. }
  59. }

创建线程和官方例子差不多,区别是QThread也是用堆分配,这样,让QThread自杀的槽就一定记得加上,否则QThread就逍遥法外了。


  1. connect(m_objThread,&QThread::finished,m_objThread,&QObject::deleteLater);

3.加锁对性能的影响

上例的runSomeBigWork2中,让一个int不停自加1,一直加到int的最大值,为了验证加锁和不加锁的影响,这里对加锁和不加锁运行了两次观察耗时的变化


  1. void ThreadObject::runSomeBigWork2()
  2. {
  3. {
  4. QMutexLocker locker(&m_stopMutex);
  5. m_isStop = false;
  6. }
  7. int count = 0;
  8. QString str = QString("%1->%2,thread id:%3").arg(__FILE__).arg(__FUNCTION__).arg((int)QThread::currentThreadId());
  9. emit message(str);
  10. int process = 0;
  11. QElapsedTimer timer;
  12. timer.start();
  13. while(1)
  14. {
  15. {
  16. QMutexLocker locker(&m_stopMutex);
  17. if(m_isStop)
  18. return;
  19. }
  20. if(m_runCount2 == count)
  21. {
  22. break;
  23. }
  24. int pro = ((float)count / m_runCount2) * 100;
  25. if(pro != process)
  26. {
  27. process = pro;
  28. emit progress(pro);
  29. emit message(QString("%1,%2,%3,%4")
  30. .arg(count)
  31. .arg(m_runCount2)
  32. .arg(pro)
  33. .arg(timer.elapsed()));
  34. timer.restart();
  35. }
  36. ++count;
  37. }
  38. }

结果如下: 

QT学习---使用多线程的两种方法(子类化QThread+子类化QObject)

这里没个横坐标的每%1进行了21474837次循环,由统计图可见,Debug模式下使用了锁后性能下降4倍,Release模式下下降1.5倍的样子

# 3.总结

  • 如果线程要用到消息循环,使用继承QObject的多线程方法更简单
  • 继承QObject的多线程不能指定父对象
  • 把所有耗时操作都作为槽函数
  • QMutex会带来一定的耗时,大概速度会降低1.5倍(Release模式).
  • //
  • 文章来源:点击打开链接
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/140319.html原文链接:https://javaforall.cn

【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛

【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...

(0)


相关推荐

  • Java 处理json经常使用代码

    Java 处理json经常使用代码

  • 常见状态码

    常见状态码常见状态码

  • Linux内核的整体架构

    Linux内核的整体架构1.前言本文是“Linux内核分析”系列文章的第一篇,会以内核的核心功能为出发点,描述Linux内核的整体架构,以及架构之下主要的软件子系统。之后,会介绍Linux内核源文件的目录结构,并和各个软件子系统对应。注:本文和其它的“Linux内核分析”文章都基于如下约定:  a)内核版本为Linux3.10.29(该版本是一个longterm的版本,会被Linux社区持续维

  • 新的Visual C++代码优化器

    微软在5月4日发布了新的高级代码优化器,服务于VisualC++的后端编译器。提高了代码性能,可以压缩代码体积,将编译器带入了一个新的境界。VisualC++的团队在博客上称,这将

    2021年12月23日
  • listagg 函数[通俗易懂]

    listagg 函数[通俗易懂]listagg函数–oracle11grelease2转载:http://xpchild.blog.163.com/blog/static/10180985920108485721969/本文描述了在oracle11grelease2版本中新增的listagg函数,listagg是一个实现字符串聚合的oracle内建函数。作为一种普遍的技术,网络上也有多种实现字符串聚合的方…

  • 动漫常用网站/APP 彻底帮你解决看动漫的烦恼[通俗易懂]

    动漫常用网站/APP 彻底帮你解决看动漫的烦恼[通俗易懂]本文是众多使用技巧中其中的一篇,全部使用技巧点击链接查看,保证你收获满满我主页中的思维导图中内容大多从我的笔记中整理而来,相应技巧可在笔记中查找原题,有兴趣的可以去我的主页了解更多计算机学科的精品思维导图整理本文可以转载,但请注明来处,觉得整理的不错的小伙伴可以点赞关注支持一下哦!本文提到的所有软件和工具,可关注公众号一起学计算机点击资源获取获得本人最常用的网站就是AGE动漫,这是我用过最好的动漫网站,资源算是最全的吧,并且大多数动漫都提供百度云链接,可下载观看,…

发表回复

您的电子邮箱地址不会被公开。

关注全栈程序员社区公众号