嵌入式Linux应用程序开发-(5)嵌入式QT多线程的简单实现(方法一)

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

        在嵌入式Linux应用程序的开发过程中,多线程永远是一个不可逃避的话题。多线程的出现,可以使一些任务处理更加方便快捷。使用多线程,可以把原来复杂庞大的系统任务,分解成多个独立的任务模块,方便开发人员管理,使程序架构更加紧凑,更加稳定。

        关于线程的简单通俗理解,请参考以下文章:http://www.ruanyifeng.com/blog/2013/04/processes_and_threads.html

在QT开发过程中,使用多线程,有两种方法:

方法一:继承QThread的 run() 函数,把复杂的循环逻辑放在run() 函数中执行。

方法二:把一个继承于QObject的类,使用moveToThread() 方法,转移到一个QThread的类对象中。

 

目标:了解QT如何分别使用两种方法,实现多线程编程。

功能:在i.MX6UL开发板上运行多线程实验,并把实验现象在显示屏上进行显示。

 

方法一:继承QThread类,重载run() 方法。

使用此方法进行QT多线程方法,有一条很重要!很重要!很重要!的规则需要记住:

继承QThread类,创建线程对象后,只有run()方法运行在新的线程里,类对象里面的其他方法都在创建QThread类的线程里运行。

简单地举一个例子:

如果在QT界面的ui线程里,使用继承了QThread的类去定义一个对象qthread,并且重载了run()函数,这个类还有其他函数。那么,调用对象qthread里面的非run()函数,这些函数就会在ui线程中执行,并不会产生新的线程ID。因此,如果要执行耗时的任务,最好把任务逻辑都写在run()函数中,否则,耗时的任务会把ui阻塞,导致ui出现卡死现象。还有一点要注意,如果要在非run()函数和run()函数里面,进行qthread对象里面的某一个变量修改,要注意进行加锁操作。因为,run()函数与非run()函数运行于不同的线程。

继承QThread类,重载run()方法,这样开启一个线程比较简单,但在开发过程中,我们更关注以下问题:

1、在ui线程中,调用了继承QThread类里面的方法,会不会造成ui卡顿。

2、在ui线程中,调用了QThread::quit() / QThread::exit()/ QThread::terminate() 会不会停止线程。

3、如何安全地退出一个线程?

4、如何正确地启动一个线程?

      > 如何正确地启动一个全局线程?

      > 如何正确地启动一个局部线程?

 

1、为了验证线程的相关问题,我们先编写一段简单的代码,使用QThread类进行线程创建。先用Qt Creator构建一个工程,命名为:005_qthread_test,关于如何构建工程,请参考“第一个嵌入式QT应用程序”的具体内容。

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

界面描述:

QThread run:点击此按钮,开始运行使用QThread类进行创建的线程,即运行run()函数。

QThread quit:点击此按钮,执行Qthread::quit()函数。

QThread Terminate:点击此按钮,以安全的方法退出线程。

QThread exit:点击此按钮,执行QThread::exit()函数。

QThread run local:点击此按钮,开始运行一个局部线程。

Clear Browser:清空显示区域的内容。

get something:点击此按钮,在ui线程中调用QThread类里面的函数,观察其线程id

set something:点击此按钮,在ui线程中调用QThread类里面的函数,观察其线程id

do something:点击此按钮,在ui线程中调用QThread类里面的函数,观察其线程id

heartbeat进度条:在ui线程中运行,观察ui线程是否有卡死现象。

thread进度条:在QThread线程中运行,显示线程运行的百分比。

信息窗口:显示程序运行时,各个线程的打印信息。

3、针对以上提出的问题,首先,我们先把已经编译下载好的程序,在开发板中运行起来,看一下实验现象。

点击这里,查看实验现象

实验现象说明:

  1. 点击 [QThread run] 按钮,ThreadFormQThread类(继承于QThread类)里面的run() 函数开始运行。run()函数首先打印线程启动信息,打印当前的线程ID,每隔1秒钟,更新一次thread进度条,打印已运行的次数,打印线程的具体信息。
  2. 点击 [QThread quit] 和 [QThread exit],调用QThread::quit() 和 QThread::exit() 函数,线程并没有停止运行,因此,以上两个函数并不能结束线程的运行。
  3. 点击 [get something] [set something] [do something] 按钮,打印出调用以上三个函数的线程ID,是ui线程ID。这就说明了,在ui线程里调用ThreadFormQThread类对象里面的函数,函数也是运行在ui线程,而非新创建的线程,只有ThreadFormQThread类对象里面的run()函数才以新的线程来运行。
  4. 点击 [QThread Terminate] 按钮,安全退出线程,即安全退出run()函数。
  5. 点击 [Clear Browser] 按钮,清除显示框的内容。
  6. 点击 [QThread run local] 按钮,启动一个新的局部线程,这个线程的父线程不是ui线程,而且,这个线程执行完后,会自动销毁线程运行时的所有资源。

 4、新建一个 ThreadFormQThread.h 文件,创建一个继承QThread的类。

class ThreadFromQThread : public QThread
{
    Q_OBJECT

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

public slots:
    void stopImmediately();   //调用此槽函数,结束进程

public:
    ThreadFromQThread(QObject* par);
    ~ThreadFromQThread();
    void setSomething();   //发送message信号,打印某些信息
    void getSomething();   //发送message信号,打印某些信息
    void setRunCount(int count);  //设置run()函数的循环次数
    void run();           //重载run()函数
    void doSomething();   //循环打印某些信息

private:
    int m_runCount;
    QMutex m_lock;
    bool m_isCanRun;
};

 

5、新建一个 ThreadFormQThread.cpp文件,编写类方法的具体实现(详细内容请参见源码),重载run()函数。以下是run()函数的具体实现。

//线程处理任务的函数体,这个run函数会在一个新的线程里运行
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);

    m_isCanRun = true;

    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;
            }
        }
    }
}

重载run()函数的实现内容,当调用QThread::start()后,这个run()函数就开始进行在新的线程里被调用,刚进入函数时,打印出当前调用run()函数的线程ID,可以看出,跟ui线程是不一样的ID。把m_isCanRun变量设置为true,这个变量用来安全退出线程的,这个变量只能在m_lock这个互斥锁里面被修改。

 

6、开始回答以上提出的问题,在ui线程中,调用了继承QThread类里面的方法,会不会造成ui卡顿?

void Widget::onButtonQthread1SetSomethingClicked()
{
    m_thread->setSomething();//在ui线程中,调用这个函数,这个函数打印来的线程ID是ui的线程ID
}

void Widget::onButtonQthread1GetSomethingClicked()
{
    m_thread->getSomething();//在ui线程中,调用这个函数,这个函数打印来的线程ID是ui的线程ID
}

void Widget::onButtonQThreadDoSomthingClicked()
{
    m_thread->doSomething();  //在ui线程中,调用这个函数,这个函数打印来的线程ID是ui的线程ID
}

如代码所示,在ui线程中,点击按钮,分别通过m_thread对象(注意:m_thread对象是在ui线程中生成的)直接调用里面的函数,里面的函数也是归属于ui线程的,从实验现象可以看出,heartbeat进度条一直在更新,验证了在ui线程中,调用了继承QThread类里面的方法,并不会造成ui卡顿。

7、在ui线程中,调用了QThread::quit() / QThread::exit()/ QThread::terminate() 会不会停止线程?

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

void Widget::onButtonQthreadTerminateClicked()
{
    //m_thread->terminate();   //调用这个函数,强制退出线程,不建议使用
    m_thread->stopImmediately(); //调用这个函数,安全退出线程
}

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

如代码所示,分别调用QThread::quit() / QThread::exit() / QThread::terminate() 进行退出线程。从实验现象可以得出,QThread::quit() 和QThread::exit() 这两个函数,并不会让线程退出,因为这两个函数只对QThread::exec()有效。QThread::terminate()则会强制退出线程,不管线程的运行情况(不建议使用这种方法)。应该使用stopImmediately()函数,安全退出线程。stopImmediately()函数的内容如下:

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

可以看出,在m_lock互斥锁的保护下,把m_isCanRun变量置为false,当run()函数的while循环遇到这个变量为false,则break当前运行的循环,结束线程。

8、如何安全地退出一个线程?

如第7点描述所示,要安全地退出一个线程,可以在外部使用stopImmediately()函数。因为是在ui主线程中调用这个函数的,并使用了互斥锁进行保护,因此,当这个函数被调用时,会马上把m_isCanRun变量置为false,这样,即可安全地退出run()函数的while循环,run()函数在返回的时候,即被视为线程结束,会发射finish()信号,槽函数onQThreadFinished()即会被调用。

//线程结束后,在窗口打印信息
void Widget::onQThreadFinished()
{
    ui->textBrowser->append("ThreadFromQThread finish");
}

9、如何正确地启动一个线程?(全局线程和局部线程)

线程的启动有多种方法,这几种方法都涉及到线程由谁(父线程)去生成,以及线程如何安全地退出。关于线程的生成和退出,首先需要搞清楚的是线程的生命周期,这个线程的生命周期是否跟ui线程一致(全局线程),还是线程只是临时生成,完成任务后就进行销毁(局部线程)。

全局线程会在创建时,把ui线程作为自己的父对象,当ui线程析构时,全局线程也会进行销毁。但此时,应该关注一个问题:当ui线程结束(窗体关闭)时,全局线程还没有结束,应当如何处理?如果没有处理好这种情况,在ui线程析构时,强行退出全局线程,会导致程序崩溃。往往这种线程的生命周期是伴随着ui线程一起开始与结束的。

局部线程,也叫临时线程,这种线程一般是要进行一些耗时任务,为了防止ui线程卡死而存在的。同样地,我们更关注以下问题:在局部线程运行期间,如果因为某些因素要停止线程,该如何安全地退出局部线程?例如,在图片打开期间(还没有完全打开),要切换图片,该如何处理。在音乐播放期间,要切换下一首音乐,应如何处理。

 

如何正确地启动一个全局线程?

由于是全局线程,因此,在ui窗体构建的时候,线程随即被构建,并且把ui窗体设置为线程的父对象。此时,需要注意的是,不能随便delete线程指针!!!因为这个线程是伴随着ui线程构建的,存在于QT的循环事件队列中,如果手动delete了线程指针,程序会很容易崩溃。正确的退出方法,可以使用 void QObject::deleteLater() [SLOT] 这个槽函数。全局线程的创建代码,如下图所示:

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,SIGNAL(clicked()),this,SLOT(onButtonQThreadClicked()));
    connect(ui->pushButton_qthread1_setSomething,SIGNAL(clicked()),this,SLOT(onButtonQthread1SetSomethingClicked()));
    connect(ui->pushButton_qthread1_getSomething,SIGNAL(clicked()),this,SLOT(onButtonQthread1GetSomethingClicked()));
    connect(ui->pushButton_qthreadQuit,SIGNAL(clicked()),this,SLOT(onButtonQthreadQuitClicked()));
    connect(ui->pushButton_qthreadTerminate,SIGNAL(clicked()),this,SLOT(onButtonQthreadTerminateClicked()));
    connect(ui->pushButton_qthreadExit,SIGNAL(clicked()),this,SLOT(onButtonQThreadExitClicked()));
    connect(ui->pushButton_doSomthing,SIGNAL(clicked()),this,SLOT(onButtonQThreadDoSomthingClicked()));
    connect(ui->pushButton_clear_broswer,SIGNAL(clicked()),this,SLOT(onButtonClearBroswerClicked()));
    connect(ui->pushButton_qthreadRunLocal,SIGNAL(clicked()),this,SLOT(onButtonQThreadRunLoaclClicked()));
    //connect(ui->pushButton_qobjectStart,&QPushButton::clicked,this,&Widget::onButtonObjectMove2ThreadClicked);
    //connect(ui->pushButton_objQuit,&QPushButton::clicked,this,&Widget::onButtonObjectQuitClicked);

    connect(&m_heart,SIGNAL(timeout()),this,SLOT(heartTimeOut()));
    m_heart.setInterval(100);

    m_thread = new ThreadFromQThread(this);//在这里创建了一个全局线程
    connect(m_thread,SIGNAL(message(QString)),this,SLOT(receiveMessage(QString)));  //接收message信号,在窗口打印线程信息
    connect(m_thread,SIGNAL(progress(int)),this,SLOT(progress(int)));//接收progress信号,更新进度条
    connect(m_thread,SIGNAL(finished()),this,SLOT(onQThreadFinished()));//接收线程结束信号,打印线程结束的消息

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

    m_currentRunLoaclThread = NULL;
}

在ui窗体构建时,创建一个全局线程对象,并关联槽函数,此时,线程对象已经构建,但线程还没有运行,run()函数还没有执行。注意,这里没有使用void QObject::deleteLater() [SLOT] 这个槽函数,而是使用了另一种方法来进行线程结束。void QObject::deleteLater() [SLOT] 这个槽函数会在局部线程那里进行使用。

10、要启动线程,点击界面上的 [QThread run] 按钮,调用onButtonQThreadClicked() 槽函数,这个函数里面,判断全局线程是否已经运行,如果没有运行,则调用QThread::start()函数,启动线程(即run()函数开始运行)。

void Widget::onButtonQThreadClicked()
{
    ui->progressBar->setValue(0);
    if(m_thread->isRunning())  //判断线程是否已经在运行
    {
        return;
    }
    m_thread->start();   //启动线程,执行线程的run()函数
}

如果在线程运行期间,重复调用QThread::start(),其实是不会进行任何处理的。在按钮的槽函数中,也进行了适当的判断。

11、启动运行一个全局线程,是很简单的,但我们更应该关注如何安全退出一个全局线程,因为这个全局线程是在ui线程中进行生成的,因此,在ui窗口析构时,应该需要判断线程是否已经运行结束(或者主动安全结束线程),才能进行 delete ui 操作。

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

在ui线程析构时,调用 stopImmediately() 安全退出线程,然后调用 QThread::wait() 等待线程结束,QThread::wait()会一直阻塞,这样才不会导致线程还没有结束就 delete ui,造成程序崩溃。

如何正确地启动一个局部线程?

12、启动一个局部线程(运行完自动销毁资源的线程),操作方法跟启动一个全局线程差不多,主要是需要多关联一个槽函数:void QObject::deleteLater() [SLOT],这个函数是局部线程安全退出的关键函数。点击 [QThread run local] 按钮,调用onButtonQThreadRunLoaclClicked()函数,启动一个局部线程。

//这个函数用来创建局部线程,所谓局部线程,就是运行完后,自动销毁的线程
void Widget::onButtonQThreadRunLoaclClicked()
{
    //判断这个局部线程是否已经存在,如果已经存在,则先退出
    if(m_currentRunLoaclThread)
    {
         m_currentRunLoaclThread->stopImmediately();
    }

    ThreadFromQThread* thread = new ThreadFromQThread(NULL);//这里父对象指定为NULL
    connect(thread,SIGNAL(message(QString)),this,SLOT(receiveMessage(QString)));//接收message信号,在窗口打印线程信息
    connect(thread,SIGNAL(progress(int)),this,SLOT(progress(int)));//接收progress信号,更新进度条
    connect(thread,SIGNAL(finished()),this,SLOT(onQThreadFinished()));//接收线程结束信号,打印线程结束的消息
    connect(thread,SIGNAL(finished()),thread,SLOT(deleteLater()));//线程结束后调用deleteLater来销毁分配的内存
    //线程销毁时,会发送destroyed信号,然后把临时变量再次赋值为NULL
    connect(thread,SIGNAL(destroyed(QObject*)),this,SLOT(onLocalThreadDestroy(QObject*)));
    thread->start();//启动线程,执行run()函数
    m_currentRunLoaclThread = thread;  //保存当前正在运行的线程

}

与全局线程不同的是,局部线程在new ThreadFromQThread(NULL)时,并没有给它指定父对象,deleteLater()槽函数与线程的finish()信号进行绑定,线程结束时,自动销毁线程创建时分配的内存资源。对于局部线程,还需要注意重复调用线程的情况。对于比较常见的需求,是在局部线程还没有执行完的时候,需要重新启动下一个线程。这时,就需要安全结束本次局部线程,再重新创建一个新的局部线程。例如:在一张图片还没有加载完成的时候,切换到下一张图片;在一首歌曲还没有播放完成的时候,切换到下一首歌曲。针对这种情况,我们使用了一个成员变量m_currentRunLoaclThread来记录当前局部线程的运行情况,当m_currentRunLoaclThread变量存在时,先结束线程,然后再生成新的局部线程。

13、除了使用成员变量来记录当前运行的局部线程,还需要关联destroy(QObject*)信号,这个信号用于当前局部线程销毁时,重新把m_currentRunLoaclThread变量置为NULL。

//局部线程销毁函数,
void Widget::onLocalThreadDestroy(QObject *obj)
{
    if(qobject_cast<QObject*>(m_currentRunLoaclThread) == obj)
    {
        m_currentRunLoaclThread = NULL;
    }
}

也可以在这个onLocalThreadDestroy(QObject *obj)的槽函数中,进行局部线程的资源回收工作。

14、至此,使用继承QThread类,重载run()方法来创建线程,已经介绍完毕,以下是这种方法的简单总结。

        a.继承QThread类,只有run()方法是运行在新的线程里,其他方法是运行在父线程里。

        b.执行QThread::start()后,再次执行该函数,不会再重新启动线程。

        c.在线程run()函数运行期间,执行QThread::quit()和QThread::exit(),不会导致线程退出。

        d.使用成员变量和互斥锁,可以进行线程的安全退出。

        e.对于全局线程,不能delete线程指针,在ui窗体析构时,应使用QThread::wait()等待全局线程执行完毕,再进行delete ui

        f.对于局部线程,要善于使用QObject::deleteLater()和QObject::destroy()来销毁线程。

 

点击这里,下载源码

 

                                                   欢迎关注公众号 【微联智控】

 

  • 4
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

工程师进阶笔记

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值