Qt多线程学习(一)——继承QThread

目录

  • 目录
  • 前言
  • 多线程
    • 多线程的优点
    • 多线程继承QThread
    • 代码分析
    • QMutexLocker
    • 如何正确启动一个线程
      • 正确的启动一个全局线程
      • 如何启动一个局部线程
      • 局部线程的需求
    • 继承QThread的总结
    • 注意
  • 总结
  • 参考链接

前言

通过继承QThreadrun函数来实现。
学习!分享!感谢!

多线程

多线程的优点

  1. 提高应用程序的响应速度。对开发图形界面程序尤为重要,当一个操作耗时很长时,整个系统都会等待这个操作,程序就不能响应键盘、鼠标、菜单等操作,而使用多线程技术可将耗时长的操作置于一个新的线程,从而避免上述问题。
  2. 使多CPU系统更加有效。当线程数不大于CPU数目时,操作系统可以调度不同的线程运行与不同的CPU上。
  3. 改善程序结构。一个既长又复杂的进程可以考虑分为多个线程,成为独立或半独立的运行部分,这样有利于程序的理解和维护。

多线程继承QThread

注意:QThread只有run函数是在新线程中。
如果QThread是在ui所在线程中生成,那么QThread的其他非run函数都是和ui线程一样的。所以QThread的继承类的其他函数尽量别要有太耗时的操作,要确保所有耗时的操作都在run函数里。在ui线程下调用QThread的非run函数和执行普通函数无区别。这时,如果这个函数要对QThread的某个变量进行变更,而这个变量在run函数也会被用到,这时就需要注意加锁的问题。

  • 继承于QThread的线程
    任何继承于QThread的线程都是通过继承QThreadrun()函数来实现多线程。因此,必须重写QThreadrun()函数,把复杂逻辑写在QThreadrun()函数中。run()是纯虚函数,是线程执行的入口,在run()函数里出现的代码会在另外的线程中被执行。run()函数是通过start()函数来实现调用的。

代码分析

这里建议直接参考源作者的github项目,我主要对作者中用到的一些技巧做记录!作为以后写多线程程序的备用!

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

在这个函数中,鼠标点击按钮后会调用这个槽函数,这个槽函数中m_thread是继承了QThread的线程,我们为了保证这个线程中的run函数只执行一次,实现调用m_thread->isRunning()来判断实例化的线程是否已经在执行了。


如何退出一个线程?作者提到在继承QThread的线程中定义如下:

private:
    QMutex m_lock;
    bool m_isCanRun;

然后,在线程中对m_isCanRun这个变量的值进行判定(当然这种情况一定是在一个循环中了),如果m_isCanRun不满足条件,就使用return退出循环

void ThreadFromQThread::run()
{
    int count = 0;
    m_isCanRun = true;  //标记可以运行
    while(1)
    {
        doSomething();

        {
            QMutexLocker locker(&m_lock);
            if(!m_isCanRun) // 在每次循环判断是否可以运行,如果不行就退出循环
            {
                return;
            }
        }
        // 注意这里在QMutexLocker外加了一层{}
    }
}

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

在调用了stopImmediately()函数的时候,就会改变对象线程的m_isCanRun的值,从而在线程run()中循环判定这个值后,即可退出线程。线程退出会发送finish信号。我们可以在主线程中连接一个槽(比如:onQThreadFinished),对线程结束进行一些操作

connect(thread, &QThread::finished, this, &Widget::onQThreadFinished);

QMutexLocker

QMutexLocker用来简化互斥量的锁定和解锁操作。在复杂函数或者异常处理代码中互斥量的锁定和解锁容易出错和难以调试。QMutexLocker就可以应用于这些情况,确保互斥量总是定义明确。
应该在程序中QMutex需要被锁定的地方创建QMutexLocker。当QMutexLocker被创建后,互斥量锁定。可以使用unlock()和relock()来解锁和再次锁定互斥量。如果互斥量被锁定,当QMutexLocker销毁的时候,自动实现互斥量的解锁。

如何正确启动一个线程

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

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

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

正确的启动一个全局线程

  • widget.h
class Widget : public QWidget
{
    Q_OBJECT

public:
    explicit Widget(QWidget *parent = 0);
    ~Widget();
private slots:

private:
    Ui::Widget *ui;
    ThreadFromQThread* m_thread;
};
  • widget.cpp
Widget::Widget(QWidget *parent) :
    QWidget(parent),
    ui(new Ui::Widget)
  ,m_objThread(NULL)
{

   ui->setupUi(this);

    //全局线程的创建
    //全局线程创建时可以把窗体指针作为父对象
    m_thread = new ThreadFromQThread(this);
    //关联线程的信号和槽
    connect(m_thread,&QThread::finished
            ,this,&Widget::onQThreadFinished);
}

由于是全局存在的线程,因此在窗体创建时就创建线程,可以把线程的父对象设置为窗体,这是需要注意,别手动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();这一句,这一句是主线程等待子线程结束才能继续往下执行,这样能确保过程是单一往下进行的,也就是不会说子线程还没结束完,主线程就destroy掉了(m_thread的父类是主线程窗口,主线程窗口如果没等子线程结束就destroy的话,会顺手把m_thread也delete这时就会崩溃了),因此wait的作用就是挂起,一直等到子线程结束。

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

如何启动一个局部线程

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

void Widget::onButtonQThreadRunLoaclClicked()
{
    //局部线程的创建的创建
    ThreadFromQThread* thread = new ThreadFromQThread(NULL);//这里父对象指定为NULL

    connect(thread, &QThread::finished
            , this, &Widget::onQThreadFinished);  // 触发主窗口的槽函数进行一些处理,和销毁无关
    connect(thread,&QThread::finished
            , thread, &QObject::deleteLater);     // 线程结束后调用deleteLater,在确认消息循环中没有这个线程的对象后,用来销毁分配的内存
    thread->start();
}

局部线程的需求

点击按钮加载图片,再次点击按钮加载另一张图片。也就是需要终结上次未执行完的线程,重新执行一个新线程。这种情况非常多见,例如一个普通的图片浏览器,都会有下一张图和上一张图这种按钮,浏览器加载图片一般都在线程里执行(否则点击超大图片时图片浏览器会类似卡死的状态),用户点击下一张图片时需要终止正在加载的当前图片,加载下一张图片。你不能要求客户要当前图片加载完才能加载下一张图片,这就几乎沦为单线程了。这时候,就需要终止当前线程,开辟新线程加载下一个图片。
原作者的处理方法是:在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设置为NULL.

也就是发出线程结束后,QObject中会产生这个线程结束的信号,同时把这个结束的线程的线程ID记录下来。我们可以根据这个线程ID来设置m_currentRunLoaclThread为NULL.

继承QThread的总结

  1. 在QThread执行start函数之后,run函数还未运行完毕,再次start不会产生后果,但是最好用isRunning()判断下。
  2. 在继承QThread的子线程运行过程中,主线程中调用quit不会有效果
  3. 程序在退出是要判断个线程是否安全退出,没退出的应该让子线程先终止。如果不进行判断,很可能程序退出时会崩溃。如果线程的父对象是窗口对象,那么在窗体的析构函数中,还需要调用wait函数等待线程完全结束再进行下面的析构。
  4. 善用QObject::deleteLaterQObject::destroyed来进行内存管理 由于多线程环境你不可预料下一步是哪个语句执行,因此,加锁和自动删除是很有用的工具,加锁是通过效率换取安全,用Qt的信号槽系统可以更有效的处理这些问题。

注意

  1. GUI程序中,主线程也被称为GUI线程,因为它是唯一一个允许执行GUI相关操作的线程。必须在创建一个QThread之前创建一个QApplication对象。
  2. 线程会因为调用printf()而持有一个控制I/O的锁,多个线程同时调用printf()在某些情况下会造成控制台输出阻塞,而用qDebug()作为控制台输出一般不会出现阻塞的问题。

总结

注意的就是局部线程和全局线程退出时的不同点。全局线程使用共享变量的方式退出,而局部线程使用QObject::deleteLater的方式退出。

参考链接

Qt多线程学习:创建多线程
Qt使用多线程的一些心得——1.继承QThread的多线程使用方法
QMutexLocker
QThread详解

你可能感兴趣的:(qt)