嵌入式Linux应用程序开发-(5)嵌入式QT多线程的简单实现(方法一)
本文的内容是拜读完以下文章后的总结,喝水不忘挖井人,感谢前辈的肩膀,让我们这些晚辈少走弯路,走得更远。如果已经理解了原作者的文章,则可完全忽略本文,感谢支持和关注。https://blog.csdn.net/czyt1988/article/details/64441443在嵌入式Linux应用程序的开发过程中,多线程永远是一个不可逃避的话题。多线程的出现,可以...
本文的内容是拜读完以下文章后的总结,喝水不忘挖井人,感谢前辈的肩膀,让我们这些晚辈少走弯路,走得更远。如果已经理解了原作者的文章,则可完全忽略本文,感谢支持和关注。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、针对以上提出的问题,首先,我们先把已经编译下载好的程序,在开发板中运行起来,看一下实验现象。
实验现象说明:
- 点击 [QThread run] 按钮,ThreadFormQThread类(继承于QThread类)里面的run() 函数开始运行。run()函数首先打印线程启动信息,打印当前的线程ID,每隔1秒钟,更新一次thread进度条,打印已运行的次数,打印线程的具体信息。
- 点击 [QThread quit] 和 [QThread exit],调用QThread::quit() 和 QThread::exit() 函数,线程并没有停止运行,因此,以上两个函数并不能结束线程的运行。
- 点击 [get something] [set something] [do something] 按钮,打印出调用以上三个函数的线程ID,是ui线程ID。这就说明了,在ui线程里调用ThreadFormQThread类对象里面的函数,函数也是运行在ui线程,而非新创建的线程,只有ThreadFormQThread类对象里面的run()函数才以新的线程来运行。
- 点击 [QThread Terminate] 按钮,安全退出线程,即安全退出run()函数。
- 点击 [Clear Browser] 按钮,清除显示框的内容。
- 点击 [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()来销毁线程。
欢迎关注公众号 【微联智控】
更多推荐
所有评论(0)