广轻电气091 发表于 2019-4-16 11:40:41

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

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

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

实验现象说明:
a.点击 按钮,ThreadFormQThread类(继承于QThread类)里面的run() 函数开始运行。run()函数首先打印线程启动信息,打印当前的线程ID,每隔1秒钟,更新一次thread进度条,打印已运行的次数,打印线程的具体信息。
b.点击 和 ,调用QThread::quit() 和 QThread::exit() 函数,线程并没有停止运行,因此,以上两个函数并不能结束线程的运行。
c.点击 按钮,打印出调用以上三个函数的线程ID,是ui线程ID。这就说明了,在ui线程里调用ThreadFormQThread类对象里面的函数,函数也是运行在ui线程,而非新创建的线程,只有ThreadFormQThread类对象里面的run()函数才以新的线程来运行。
d.点击 按钮,安全退出线程,即安全退出run()函数。
e.点击 按钮,清除显示框的内容。
f.点击 按钮,启动一个新的局部线程,这个线程的父线程不是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() 这个槽函数。
全局线程的创建代码,如下图所示:
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() 这个槽函数,而是使用了另一种方法来进行线程结束。void QObject::deleteLater() 这个槽函数会在局部线程那里进行使用。

10、要启动线程,点击界面上的 按钮,调用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() ,这个函数是局部线程安全退出的关键函数。
点击 按钮,调用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()方法来创建线程,已经介绍完毕,以下是这种方法的简单总结。
(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()来销毁线程。

zzh4933 发表于 2019-4-16 14:48:03

太高大上了{:lol:}{:lol:}{:lol:}{:lol:}{:lol:}{:lol:}{:lol:}{:lol:}{:lol:}

广轻电气091 发表于 2019-4-16 14:53:23

zzh4933 发表于 2019-4-16 14:48
太高大上了

不会呢,静下心来折腾一下,也不是很复杂{:lol:}

Excellence 发表于 2019-4-16 14:55:12

MARK.................

广轻电气091 发表于 2019-4-16 15:02:39

Excellence 发表于 2019-4-16 14:55
MARK.................

感谢关注

prince2010 发表于 2019-4-16 16:20:28

顶楼主{:victory:}

广轻电气091 发表于 2019-4-16 16:24:23

prince2010 发表于 2019-4-16 16:20
顶楼主

感谢支持

flash3g 发表于 2019-4-16 16:29:54

谢谢分享,mark..学习学习

广轻电气091 发表于 2019-4-16 16:31:40

flash3g 发表于 2019-4-16 16:29
谢谢分享,mark..学习学习

感谢关注和支持

zhongsandaoren 发表于 2019-4-17 09:55:51

顶起来{:victory:}

meirenai 发表于 2019-4-17 10:51:56

顶楼主,继续学习

广轻电气091 发表于 2019-4-17 11:15:53

meirenai 发表于 2019-4-17 10:51
顶楼主,继续学习

感谢支持

jianbo513 发表于 2019-4-17 11:27:56

支持一下!!!

广轻电气091 发表于 2019-4-17 11:38:06

jianbo513 发表于 2019-4-17 11:27
支持一下!!!

感谢支持

xiaomu 发表于 2019-4-17 13:00:08

支持一下, 感谢楼主的分享!

meirenai 发表于 2019-4-17 13:25:16

请教楼主,如果不是图形界面的应用一般使用 qt 还是 直接用 liunx c 来写呢?

广轻电气091 发表于 2019-4-17 13:30:47

meirenai 发表于 2019-4-17 13:25
请教楼主,如果不是图形界面的应用一般使用 qt 还是 直接用 liunx c 来写呢? ...

这个主要根据业务的复杂程度来判断,如果是面向过程的应用程序,可以使用Linux C进行开发。如果业务有一定的复杂程度,建议使用C++这类面向对象的语言进行开发

广轻电气091 发表于 2019-4-17 13:31:24

xiaomu 发表于 2019-4-17 13:00
支持一下, 感谢楼主的分享!

感谢支持

meirenai 发表于 2019-4-17 17:43:11

如果厂家没有 qt 的移植,自己移植是不是很麻烦,问题多多?
我现在用 ec20 二次开发,移远只提供 linux c 的 sdk,是不是所有都要自己封装(gpio timer 拨号上网等等。。)

广轻电气091 发表于 2019-4-18 08:24:15

meirenai 发表于 2019-4-17 17:43
如果厂家没有 qt 的移植,自己移植是不是很麻烦,问题多多?
我现在用 ec20 二次开发,移远只提供 linux c...

如果是原厂没有提供,自己移植可能会稍有困难。如果原厂没有大力度支持,建议不要使用小众芯片进行开发。

Jmhh247 发表于 2019-4-18 08:51:31

顶楼主,讲的很好!

广轻电气091 发表于 2019-4-18 09:08:11

感谢支持

广轻电气091 发表于 2019-4-18 09:08:35

Jmhh247 发表于 2019-4-18 08:51
顶楼主,讲的很好!

感谢支持

llysc 发表于 2019-4-18 09:37:30

收藏先,多谢楼主!

广轻电气091 发表于 2019-4-18 09:41:23

llysc 发表于 2019-4-18 09:37
收藏先,多谢楼主!

感谢支持

jiang887786 发表于 2019-4-19 14:37:39

继续更新啊,楼主。就等着看你的课程!

广轻电气091 发表于 2019-4-19 14:53:53

jiang887786 发表于 2019-4-19 14:37
继续更新啊,楼主。就等着看你的课程!

最近因为工作的事情要忙,更新进度会慢点,但不会缺席噢,感谢支持和关注!

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

源码下载路径已更改:点击这里

liujinhan 发表于 2019-4-29 14:13:28

大力支持LZ!!~~

早几年看到这个教材,可能命运就被你改变了!

广轻电气091 发表于 2019-4-29 14:27:46

liujinhan 发表于 2019-4-29 14:13
大力支持LZ!!~~

早几年看到这个教材,可能命运就被你改变了!

感谢支持,命运掌握在你自己手中呢。种一棵好树最好的时间,一是十年前,其次是现在。

abnerle 发表于 2019-5-17 14:38:30

QT运行起来,总觉得有点笨重,应该轻量化瘦身一下

广轻电气091 发表于 2019-5-17 14:44:31

abnerle 发表于 2019-5-17 14:38
QT运行起来,总觉得有点笨重,应该轻量化瘦身一下

是有点笨重,但跨平台,做界面也很方便,有利有弊
页: [1]
查看完整版本: 嵌入式Linux应用程序开发-(5)嵌入式QT多线程的简单实现...