广轻电气091 发表于 2019-4-22 15:06:48

嵌入式Linux应用程序开发-(6)嵌入式QT多线程的简单实现...

本帖最后由 广轻电气091 于 2019-4-22 15:14 编辑

嵌入式QT多线程的简单实现(方法二)
本文的内容是拜读完以下文章后的总结,喝水不忘挖井人,感谢前辈的肩膀,让我们这些晚辈少走弯路,走得更远。如果已经理解了原作者的文章,则可完全忽略本文,感谢支持和关注。
https://blog.csdn.net/czyt1988/article/details/71194457

上一篇文章介绍了使用继承QThread类,重载run()函数的方法来实现多线程,这种方法是QT实现多线程编程的传统方法,
但从QT4.8以后的版本开始,QT官方并不是很推荐使用这种方法,而是极力推荐使用继承QObject类的方法来实现多线程,因为QObject类比QThread类更加灵活。
当然,使用哪种方法实现多线程,需要根据具体的实际情况而定。
关于上一篇文章,请点击这里。嵌入式Linux应用程序开发-(5)嵌入式QT多线程的简单实现(方法一)

QObject类是QT框架里面一个很重要的基本类,除了QT的关键技术信号与槽,QObject还提供了事件系统和线程操作接口的支持。
对QObject类,可以使用QObject类里面的方法moveToThread(QThread *targetThread),把一个没有父级的继承于QObject的类转移到指定的线程中运行。
使用继承QObject来实现多线程,默认支持事件循环(QT里面的QTimer、QTcpSocket等等,均支持事件循环)。
而如果使用QThread类来实现多线程,则需要调用QThread::exec()来支持事件循环,否则,那些需要事件循环支持的类都无法实现信号的发送。
因此,如果要在多线程中使用信号和槽,那就直接使用QObject来实现多线程。

不管使用方法一还是方法二,在QT中创建多线程是比较简单的,难点在于如何安全地退出线程和释放线程资源。
在创建完线程之后,使用方法二(继承QObject)来创建的线程,不能在ui线程(主线程)中直接销毁,而是需要通过deleteLater() 进行安全销毁。
先来总结一下,如何使用继承QObject的方法来创建多线程:
(1)写一个继承QObject的类,并把复杂耗时的操作声明为槽函数。
(2)在主线程(ui线程)中new一个继承Object类的对象,不设置父类。
(3)声明并new一个QThread对象。(如果QThread对象没有new出来,则需要在QObject类析构的时候使用QThread::wait()等待线程完成。如果是通过堆分配(new方式),则可以通过deleteLater来进行释放)
(4)使用QObject::moveToThread(QThread*)方法,把QObject对象转移到新的线程中。
(5)把线程的finished()信号与QObject的deleteLater()类连接,这个是安全释放线程的关键,不连接的话,会导致内存泄漏。
(6)连接其他信号槽,如果是在连接信号槽之前调用moveToThread,不需要处理connect函数的第五个参数,这个参数是表示信号槽的连接方式;否则,如果在连接所有信号槽之后再调用moveToThread,则需要显式声明Qt::QueuedConnection来进行信号槽连接。
(7)完成一系列初始化后,调用QThread::start()来启动线程。
(8)在完成一系列业务逻辑后,调用QThread::quit()来退出线程的事件循环。

使用继承QObject的方法来创建和启动线程,这种方法比使用QThread来创建线程要灵活得多。
使用这种方法创建线程,整个线程对象都在新的线程中运行,而不像QThread那样,只有run()函数在新的线程中运行。
QObject自身的信号槽和事件处理机制,可以灵活地应用到业务逻辑中。

下面,我们基于方法一(继承QThread)的例程,使用继承QObject的方法来创建线程。
1、先用Qt Creator构建一个工程,命名为:006_qthread_object,关于如何构建工程,请参考“第一个嵌入式QT应用程序”的具体内容。

2、双击打开“widget.ui”文件,构建界面,构建后的界面如下图所示:

界面描述:
:发送信号启动耗时任务1
:发送信号启动耗时任务2
:修改变量,退出耗时任务的运行
:清空信息显示窗口

3、创建一个ThreadObject.h头文件,在头文件定义一个继承于QOBject的类ThreadObject,类的具体成员变量和成员函数,如下所示:
class ThreadObject : public QObject
{
    Q_OBJECT

signals:
    void message(const QString& info);//通过此信号,发送需要打印的message消息
    void progress(int present);//通过此信号,发送ProgressBar的进度百分比

public:
    ThreadObject(QObject* parent = NULL);
    ~ThreadObject();
    void setRunCount(int count);//设置运行次数
    void stop();


public slots:
    void runSomeBigWork1();   //线程的while循环,在里面执行耗时任务
    void runSomeBigWork2();      //线程的while循环,在里面执行耗时任务

private:
    int m_runCount;       //while循环的运行次数
    int m_runCount2;      //while循环的运行次数
    bool m_isStop;      //线程停止标志位

    QMutex m_stopMutex;

    void msleep(unsigned int msec);//线程休眠函数
};


4、创建一个ThreadObject.cpp文件,ThreadObject类里面的成员函数,均在这个文件里面实现。代码如下所示:
//线程的耗时操作都在这里进行
void ThreadObject::runSomeBigWork1()
{
    {
      QMutexLocker locker(&m_stopMutex);
      m_isStop = false;
    }

    int count = 0;
    QString str = QString("%1->%2,thread id:%3").arg(__FILE__).arg(__FUNCTION__).arg((int)QThread::currentThreadId());
    emit message(str);
    int process = 0;
    emit progress(process);
    while(1)
    {
      {
            QMutexLocker locker(&m_stopMutex);
            if(m_isStop)    //通过m_isStop变量来退出线程
                return;
      }

      int pro = ((float)count / m_runCount) * 100;
      if(pro != process)
      {
            process = pro;
            emit progress(((float)count / m_runCount) * 100);   //线程运行的百分比
            emit message(QString("Object::run times:%1,m_runCount:%2").arg(count).arg(m_runCount));
      }
      msleep(1000);
      count++;

      if(m_runCount < count)   //线程达到最大的运行次数,则退出
      {
            break;
      }
    }
}

//线程的耗时操作都在这里进行
void ThreadObject::runSomeBigWork2()
{
    {
      QMutexLocker locker(&m_stopMutex);
      m_isStop = false;
    }

    int count = 0;
    QString str = QString("%1->%2,thread id:%3").arg(__FILE__).arg(__FUNCTION__).arg((int)QThread::currentThreadId());
    emit message(str);
    int process = 0;
    QElapsedTimer timer;
    timer.start();
    while(1)
    {
      {
            QMutexLocker locker(&m_stopMutex);
            if(m_isStop)
                return;
      }

      int pro = ((float)count / m_runCount2) * 100;
      if(pro != process)
      {
            process = pro;
            emit progress(pro);
            emit message(QString("%1,%2,%3,%4").arg(count).arg(m_runCount2).arg(pro).arg(timer.elapsed()));
            timer.restart();
      }

      msleep(1000);
      count++;

      if(m_runCount2 < count)   //线程达到最大的运行次数,则退出
      {
            break;
      }
    }
}

线程的耗时操作都在这两个函数中进行,这两个函数都是通过ui线程中的信号进行触发的。同时,为了对线程进行停止,这里使用了一个m_isStop的成员变量。
通过在ui线程中,调用stop()函数修改m_isStop变量,达到退出线程的目的。注意,修改m_isStop变量时,要进行互斥锁操作。

5、在widget.cpp文件中,ui窗口进行构造时,先设置进度条的初始状态,并关联定时器的超时槽函数,这个定时器用来监控ui线程是否卡死。代码如下所示:
Widget::Widget(QWidget *parent) :
    QWidget(parent),
    ui(new Ui::Widget)
{
    ui->setupUi(this);

    m_obj = NULL;
    m_objThread = NULL;

    ui->progressBar->setRange(0,100);
    ui->progressBar->setValue(0);
    ui->progressBar_heart->setRange(0,100);
    ui->progressBar_heart->setValue(0);

    //启动一个定时器,用来监视ui线程是否有卡死
    connect(&m_heart,SIGNAL(timeout()),this,SLOT(heartTimeOut()));
    m_heart.setInterval(100);

    m_heart.start();   //启动定时器,不断更新heartbeat进度条

    //打印出 ui 的线程id
    receiveMessage(QString("%1->%2,current ui id is:%3").arg(__FUNCTION__).arg(__FILE__).arg((int)QThread::currentThreadId()));
}


6、点击 或 按钮,即可调用startObjThread()启动线程,然后通过发送信号,唤起线程耗时操作的槽函数。startObjThread()函数的具体实现如下所示:
//使用moveToThread的方式启动一个线程
void Widget::startObjThread()
{
    if(m_objThread)   //如果这个线程已经存在,则不再启动
    {
      return;
    }
    m_objThread= new QThread();    //创建一个QThread对象
    m_obj = new ThreadObject();    //使用继承于QObject的类创建一个对象
    m_obj->moveToThread(m_objThread);    //把继承于QObject的类对象转移到新的线程运行

    //关联deleteLater槽函数,这是线程安全退出的关键
    connect(m_objThread,SIGNAL(finished()),m_objThread,SLOT(deleteLater()));
    connect(m_objThread,SIGNAL(finished()),m_obj,SLOT(deleteLater()));

    //关联两个信号,用于启动线程里面的耗时操作
    connect(this,SIGNAL(startObjThreadWork1()),m_obj,SLOT(runSomeBigWork1()));
    connect(this,SIGNAL(startObjThreadWork2()),m_obj,SLOT(runSomeBigWork2()));

    //关联两个信号,用于更新进度条和打印信息
    connect(m_obj,SIGNAL(progress(int)),this,SLOT(progress(int)));
    connect(m_obj,SIGNAL(message(QString)),this,SLOT(receiveMessage(QString)));

    m_objThread->start();
}
这里需要注意,QThread对象是通过new方法在堆内创建的,线程finished()退出的时候,需要关联deleteLater槽函数进行线程的安全退出和线程资源回收。
否则,线程所占用的内存资源就不会进行释放,长时间下去,会造成内存资源泄漏。

7、至此,使用继承QObject这种方法来实现多线程已经介绍完毕,这种方法比使用继承QThread更加方便灵活,使用这种方法的总结如下:
(1)如果线程里面使用到消息循环,信号槽,事件队列等等操作,建议使用QObject来创建线程。
(2)继承QObject的类对象,都不能指定其父对象。
(3)新线程里面耗时操作,都定义为槽函数,方便通过信号启动线程。
(4)操作线程里面的成员变量时,为了安全,需要加锁后再进行操作。
(5)互斥锁QMutex会带来一点性能损耗。

8、把程序编译完后,下载到开发板运行,运行现象如下图所示:

实验现象说明:
(1)程序开始运行时,先打印出ui的线程ID。
(2)点击按钮,耗时任务work1开始运行,并打印出运行的线程ID。
(3)点击按钮,耗时任务work2开始运行,并打印出运行的线程ID。
(4)点击按钮,耗时任务停止运行,并打印出stop()函数所在的线程ID。
(5)在work1运行期间,如果开启work2任务,则work1任务会中断,反之亦然。
(6)任务work1和任务work2均由同一个线程管理并运行。
(7)stop()函数与新建线程不在同一个线程内,stop()函数是归属于ui线程的。

点击这里,下载工程源码

广轻电气091 发表于 2019-4-23 08:13:39

本章节pdf下载:

穿越时空 发表于 2019-4-23 09:04:05

广轻电气091 发表于 2019-4-23 08:13
本章节pdf下载:

楼主,我看着你的教程,卡在了qmake,新手{:mad:}

广轻电气091 发表于 2019-4-23 09:17:39

穿越时空 发表于 2019-4-23 09:04
楼主,我看着你的教程,卡在了qmake,新手

你使用的是哪个平台?遇到什么问题了?

广轻电气091 发表于 2019-4-29 13:57:36

源码下载路径已更改:点击这里
页: [1]
查看完整版本: 嵌入式Linux应用程序开发-(6)嵌入式QT多线程的简单实现...