找回密码
 立即注册
Qt开源社区 门户 查看内容

(5)嵌入式QT多线程的简单实现(方法一)

2019-8-11 16:13| 发布者: admin| 查看: 1029| 评论: 1

摘要: 点击上方蓝字,关注微联智控可点击右上角的 …,分享这篇文章 本文的内容是拜读完以下文章后的总结,喝水不忘挖井人,感谢前辈的肩膀,让我们这些晚辈少走弯路,走得更远。如果已经理解了原作者的文章,则可完全 ...
点击上方蓝字,关注微联智控

可点击右上角的 …,分享这篇文章

        本文的内容是拜读完以下文章后的总结,喝水不忘挖井人,感谢前辈的肩膀,让我们这些晚辈少走弯路,走得更远。如果已经理解了原作者的文章,则可完全忽略本文,感谢阅读。

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的类对象中。

方法一:继承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、如何正确地启动一个线程?

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

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

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

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

以下是应用程序的开发过程

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



实验现象说明:

a. 点击 [QThread run] 按钮,ThreadFormQThread类(继承于QThread类)里面的run() 函数开始运行。run()函数首先打印线程启动信息,打印当前的线程ID,每隔1秒钟,更新一次thread进度条,打印已运行的次数,打印线程的具体信息。

b. 点击 [QThread quit] 和 [QThread exit],调用QThread::quit() 和 QThread::exit() 函数,线程并没有停止运行,因此,以上两个函数并不能结束线程的运行。

c. 点击 [get something] [set something] [do something] 按钮,打印出调用以上三个函数的线程ID,是ui线程ID。这就说明了,在ui线程里调用ThreadFormQThread类对象里面的函数,函数也是运行在ui线程,而非新创建的线程,只有ThreadFormQThread类对象里面的run()函数才以新的线程来运行。

d. 点击 [QThread Terminate] 按钮,安全退出线程,即安全退出run()函数。

e. 点击 [Clear Browser] 按钮,清除显示框的内容。

f. 点击 [QThread run local] 按钮,启动一个新的局部线程,这个线程的父线程不是ui线程,而且,这个线程执行完后,会自动销毁线程运行时的所有资源。

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



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



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

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



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

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



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



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

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

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



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

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

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

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

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

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



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

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



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

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



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

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

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



与全局线程不同的是,局部线程在new ThreadFromQThread(NULL)时,并没有给它指定父对象,deleteLater()槽函数与线程的finish()信号进行绑定,线程结束时,自动销毁线程创建时分配的内存资源。

对于局部线程,还需要注意重复调用线程的情况。对于比较常见的需求,是在局部线程还没有执行完的时候,需要重新启动下一个线程。这时,就需要安全结束本次局部线程,再重新创建一个新的局部线程。例如:在一张图片还没有加载完成的时候,切换到下一张图片;在一首歌曲还没有播放完成的时候,切换到下一首歌曲。针对这种情况,我们使用了一个成员变量m_currentRunLoaclThread来记录当前局部线程的运行情况,当m_currentRunLoaclThread变量存在时,先结束线程,然后再生成新的局部线程。

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



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

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

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

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

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

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

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

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

-- END --




长按识别二维码,关注微联智控




----------------------------------------------------------------------------------------------------------------------
我们尊重原创,也注重分享,文章来源于微信公众号:微联智控,建议关注公众号查看原文。如若侵权请联系qter@qter.org。
----------------------------------------------------------------------------------------------------------------------

1人点赞鲜花

握手

雷人

路过

鸡蛋

刚表态过的朋友 (1 人)


公告
可以关注我们的微信公众号yafeilinux_friends获取最新动态,或者加入QQ会员群进行交流:190741849、186601429(已满) 我知道了