浅谈C++中的多线程(三):Qt中的多线程应用

本篇文章围绕以下几个问题展开:

  1. 何为进程?何为线程?两者有何区别?
  2. 何为并发?C++中如何解决并发问题?C++中多线程的语言实现?
  3. 同步互斥原理以及多进程和多线程中实现同步互斥的两种方法
  4. Qt中的多线程应用

前三个问题已经在之前两篇文章中讲解,本篇文章作为结尾篇来讲解最后一个问题:Qt中的多线程应用。

四.Qt中的多线程应用 

1.引入

程序由单线程到多线程,就像以前是一个人干活,变成了现在是多个人一起干活。映射到具体代码中就是,以前各部分代码只能按照顺序来执行,现在可以多个部分的代码同时执行,这岂不爽哉!Qt中引入多线程是非常有必要的,因为Qt开发的应用程序是事件驱动型的,一旦某个事件处理函数处理时间太久,就会造成其它的事件得不到及时处理。

跟C++11中很像的是,Qt中使用QThread来管理线程,一个QThread对象管理一个线程,在使用上有很多跟C++11中相似的地方,但更多的是Qt中独有的内容。另外,QThread对象也有消息循环exec()函数,即每个线程都有一个消息循环,用来处理自己这个线程的事件。

2.Qt中实现多线程的两种方法

2.1.派生QThread类对象的方法

首先,以文字形式来说明需要哪几个步骤。

  1. 自定义一个自己的类,使其继承自QThread类;
  2. 在自定义类中覆写QThread类中的虚函数run()。

 这很可能就是C++中多态的使用。补充一点:QThread类继承自QObject类。

这里要重点说一下run()函数了。它作为线程的入口,也就是线程从run()开始执行,我们打算在线程中完成的工作都要写在run()函数中,个人认为可以把run()函数理解为线程函数。这也就是子类覆写基类的虚函数,基类QThread的run()函数只是简单启动exec()消息循环,关于这个exec()后面有很多东西要讲,请做好准备。

然后,请看具体的代码,相信你能更加充分的理解这种方式

#include <QApplication>
#include <QThread>
#include <QDebug>

class MyThread:public QThread
{
    public:
        void run()
        {
            qDebug()<<"child thread begin"<<endl;
            qDebug()<<"child thread"<<QThread::currentThreadId()<<endl;
            QThread::sleep(5);
            qDebugu()<<"QThread end"<<endl;
            this->exec();
        }
};

int main(int argc,char ** argv) //mian()作为主线程
{
    QApplication app(argc,argv);

    MyThread thread; //创建一个QThread派生类对象就是创建了一个子线程
    thread.start(); //启动子线程,然后会自动调用线程函数run()

    qDebug()<<"main thread"<<QThread::currentThreadId()<<endl;
    QThread::sleep(5);
    qDebugu()<<"main thread"<<QThread::currentThreadId()<<endl;

    thread.quit(); //使用quit()或者exit()使得子线程能够退出消息循环,而不至于陷在子线程中
    thread.wait(); //等待子线程退出,然后回收资源
                   //thread.wait(5000); //设定等待的时间
    
    return app.exec();    
}

好了,下面对这段程序进行详细解释。 

首先要创建一个线程,也就是创建一个QThread派生类对象,然后调用start()函数来启动线程,此时会自动调用run()来执行线程中的工作。

可以使用QThread::currentThreadId()来查看当前线程的ID,无论是子线程还是主线程,不同线程其ID是不同的。注意,这是一个静态函数,因此可以不经过对象来调用。

如果run()函数中没有执行exec()消息循环函数,那么run()执行完了也就意味着子线程退出了。一般在子线程退出的时候需要主线程去回收资源,可以调用QThread的wait(),使主线程等待子线程退出,然后回收资源。这里wait()是一个阻塞函数,有点像C++11中的join()函数。

但是!但是!但是!run()函数中调用了exec()函数,exec()是一个消息循环,也可以叫做事件循环,也是会阻塞的,相当于一个死循环使子线程卡在这里永不退出,必须调用QThread的quit()函数或者exit()函数才可以使子线程退出消息循环,并且有时还不是马上就退出,需要等到CPU的控制权交给线程的exec()。

所以先要thread.quit();使退出子线程的消息循环, 然后thread.wait();在主线程中回收子线程的资源。

值得注意的有两点:子线程的exet()消息循环必须在run()函数中调用;如果没有消息循环的话,则没有必要调用quit( )或者exit(),因为调用了也不会起作用。

OK,基本上这种通过QThread派生类对象来创建线程的方法就讲解完了。下面不急于看第二种创建线程的方式,先插播一个重要知识点。

2.2.插播一个重要知识点:exec()消息循环

个人认为,exec()这个点太重要了,同时还不太容易理解。

上面2.1.中的代码中有两个exec(),我们讲“一山不容二虎”,放在这里就是说,一个线程中不能同时运行两个exec(),否则就会造成另一个消息循环得不到消息。像QDialog模态窗口中的exec()就是因为在主线程中同时开了两个exec(),导致主窗口的exec()接收不到用户的消息了。但是!但是!但是!我们这里却没有任何问题,因为它们没有出现在同一个线程中,一个是主线程中的exec(),一个是子线程中的exec()。

我们说,每一个进程都对应着一条河流——消息河流,消息循环exec()就像是一个架在河流上的抽水泵,消息(也可以理解为事件)从河流的一端流向另一端,就会被exec()捕获到,然后将CPU的控制权交给对应的事件处理函数,执行完毕,控制权又返回给exec(),这样消息循环可以继续接收消息。下面用一张图来描述吧:

 因为现在有两个线程——主线程和子线程,可以看到对应着两条消息河流,每条河流中都架设着一台抽水泵——exec()消息循环用来捕获消息(事件)。

再来看一下QDialog模态窗口下的消息河流:

这是在一个线程中同时开了两个exec()消息循环,也就是在一个消息河流中假设了两台抽水泵。因为QDialog::exec()处于上游,很显然消息都被它给截获了,也就是CPU的控制权全部被它给占用了。关键是QDialog::exec()很自私,它接收到一个事件之后如果发现不是自己想要的,就直接丢弃了,根本不会让它流到QApplication::exec()那里,所以QApplication::exec()什么都接收不到,直到QDialog::exec()退出。 这也就是为什么模态窗口会屏蔽其他窗口的消息。

第一种创建线程的方式需要在run()中显式调用exec(),但是exec()有什么作用呢,目前还看不出来,需要在第二种创建线程的方式中才能知道。

关于每个进程中的消息河流,更标准一点的说法是消息队列。

在GUI应用程序开发中,一般是将界面相关的内容设置为主线程,那么跟界面相关的消息,比如用户点击鼠标的事件,键盘输入的事件都会被发到主线程对应的消息队列中,被主线程中的exec()所获取,具体说有可能被QDialog的exec()获取,也有可能被QApplication的exec()获取。而子线程是不会接收到这些消息(事件)的,因为操作系统会把消息发到对应进程的消息队列中,一般子线程用来处理数据和完成计算,做一些后台工作。

2.3.使用信号与槽方式来实现多线程

刚讲完使用QThread派生类对象的方法创建线程,现在就要来说它一点坏话。这种方法存在一个局限性,只有一个run()函数能够在线程中去运行,但是当有多个函数在同一个线程中运行时,就没办法了,至少实现起来很麻烦。所以,当当当当,下面将介绍第二种创建线程的方式:使用信号与槽的方式,也就是把在线程中执行的函数(我们可以称之为线程函数)定义为一个槽函数。

 仍然是首先以文字形式说明这种方法的几个步骤。

注意:必须通过发射信号来让槽函数在子线程中执行,发射的信号存放在子线程消息队列中。要知道发射的信号会经过一个包装,记录其发送者和接收者等信息,操作系统会根据该信号的接收者将信号放在对应线程的消息队列中。

  1. 继承QObject来自定义一个类,该类中实现一个槽函数,也就是线程函数,实现线程要完成的工作;
  2. 在主线程(main函数)中实例化一个QThread对象,仍然用来管理子线程;
  3. 用继承自QObject的自定义类来实例化一个对象,并通过moveToThread将自己放到线程QThread对象中;
  4. 使用connect()函数链接信号与槽,因为一会线程启动时会发射一个started()信号; 
  5. 调用QThread对象的start()函数启动线程,此时会发出一个started()信号,然后槽函数就会在子线程中执行了。

然后下面看具体的代码吧。

#include <QThread>
#include <QDebug>
class MyWork:public QObject
{
    
}

因为本人这周五有一个面试,时间比较紧张,暂时停止更博。面试完之后会抽出时间来更新!谢谢!

发布了34 篇原创文章 · 获赞 22 · 访问量 7193

猜你喜欢

转载自blog.csdn.net/lizun7852/article/details/88783440