Qt5中对于多线程和事件的一些理解

目录

一.Qt5线程理解

二.Qt5正确控制线程开始退出


一.Qt5线程理解

读了《Qt学习之路2》多线程相关章节,说实话读了5遍之后才理解多线程和QObject之间的关系。个人感觉理解Qt5多线程最重要的是理解书中所谓的线程依赖的关系。线程依赖是相对于QObject对象而言的,也就是说我们要辨别清楚某个QObject的所依赖的线程是哪一个。

Qt5因为每个QThread的子类的run函数都默认调用了exec(),每个线程都有各自单独的事件循环机制。所以Qt5中推荐我们实现多线程不再是像之前的自定义一个类继承QThread,然后在QThread中的run函数实现自己的业务代码。而是利用线程自己的事件循环机制来调用依赖这个线程的QObject的slot槽函数实现多线程的业务代码。那么如何让一个QObject依赖于一个线程呢?有两种方法:

1.在线程的run()函数中创建的QObject必定依赖这个线程。

2.使用QObject::moveToThread(pThread)函数将QObject依赖于pThread所代表的线程。

当一个QObject依赖于一个线程的时候,那么QObject的slot槽函数也将会在所依赖线程的run函数中去执行,也就达到了我们在线程中执行一个耗时操作而不影响Qt主GUI线程了,即使slot槽函数中是一些耗时操作,它阻塞的也只是所依赖线程的事件循环。总的来说也就是QObject在哪个线程中创建的,QObject依赖哪个线程(除非使用moveToThread修改了线程依赖性),那么QObject的槽函数就会在他依赖的那个线程的run()函数中执行,并且如果一个槽函数非常耗时阻塞了这个线程,那么下一个槽函数会一直等待执行直到前一个槽函数执行完毕。下面列举一个简单的例子

//===============继承QThread的工作线程类=======================
#ifndef CKVTHREAD_H
#define CKVTHREAD_H

#include <QObject>
#include <QThread>

class CKvThread : public QThread
{
public:
    CKvThread(){
        qDebug()<<"构造函数"<<endl;
    }
    ~CKvThread(){
        qDebug()<<"析构函数"<<endl;
    }
};
#endif // CKVTHREAD_H
//=============================================================


//===============实现具体业务代码QObject类=======================
class CKvOptimize : public QObject
{
    Q_OBJECT
public:
    CKvOptimize();
private slots:
    //耗时的槽函数
    void slot_Solve(void){
        int i = 0;
        while(i < 30){//slot耗时30s
            Sleep(1000);//睡眠1s
            i++;
        }
    }
};
//=============================================================


//Qt主GUI线程
MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    pTestThread = new CKvThread();
    pTestOpti = new CKvOptimize();

    //使得pTestOpti这个QObject依赖线程pTestThread所代表的线程
    pTestOpti->moveToThread(this->pTestThread);

    //使用信号槽机制链接信号和槽函数
    connect(this->ui->bt_testOpti/*主界面上的一个按钮*/ , SIGNAL(clicked(bool)) , this->pTestOpti , SLOT(slot_Solve(void)));

    //开始线程事件循环
    pTestThread->start();
}

在这个例子中pTestOpti这个QObject是在主线程中创建的,所以应该依赖于主线程,但是由于使用了moveToThread将其依赖性线程设置为了pTestThread所代表的线程,所以最终pTestOpti依赖pTestThread所代表的线程。而pTestOpti得耗时槽函数slot_solve()也将在pTestThread的run()中执行而不会阻塞主线程。

另外一个重要的点是需要正确理解Qt的四种信号槽链接模式:

1.Qt::DirectConnection直接链接:槽函数将在发送信号的线程直接执行调用槽函数。

2.Qt::QueuedConnection队列链接:像信号接受对象所依赖的线程发送一个事件,该线程注意处理自己的事件循环队列中的事件在合适的时候执行调用槽函数。

3.Qt::BlockingQueuedConnection阻塞队列链接:和第二种链接方式一样,但是不同的是发送信号的线程会阻塞直到槽函数被调用执行完成返回。

4.Qt::AutoConnection自动链接(默认链接方式):如果发送信号的线程和信号接受对象所依赖的线程相同则使用Qt::DirectConnection否则使用t::QueuedConnection。

注意上面的解释中使用的字眼是“发送信号的线程”而不是“发送信号对象所依赖的线程”。而且可以看出链接方式中除了自动链接模式以外,槽函数在哪个线程中执行和发送者没有关系,其实只需要关心接受对象所依赖的线程是哪一个即可。

附录注意事项:

1.QObject和其子对象必须都依赖于同一个线程,所以不能对QObject的子对象调用moveToThread函数仅仅只改变子对象的线程依赖性。

2.QThread对象并不是线程本身,它只是用于管理它所代表的线程。所以正确的用法应该是在它所依赖的线程中被使用(通常就是创建QThread对象的线程)而不应该在它所代表的线程中使用。

3.事件循环是在Qt组件层次传播的,而不是依靠类的继承机制来传播。正因为事件是在组件层次传播的,所以事件会传递给各个窗口。

4.在使用组件的时候我们只需要关心信号槽就可以了,当我们需要自定义组件的时候才需要关心事件,因为实际上信号需要在我们自定义组件中的事件处理函数中去发出。

5.事件其实是操作系统的特定程序进程来捕获的,然后通过进程间通信机制将事件信息传递到Qt应用程序的。Qt的主事件循环处理函数QApplication.exec()其实就是不断的处理操作系统发送到Qt应用程序事件队列中的事件。

 

二.Qt5正确控制线程开始退出

上面我们已经正确理解了工作对象和依赖线程之间的关系,如果一个QObject对象依赖于一个线程,那么这个QObject的所有槽函数都会在这个线程的run()函数也就是在这个线程中运行,而不会阻塞主线程工作。这样也就达到了多线程工作的目的。那么问题来了我们需要怎么样正确的退出一个正在运行的线程呢?如果在线程还在工作的时候退出程序会出现程序崩溃的现象。如下所示来控制一个线程的开始和退出。

//=============================QObject工作对象==================================
void CKvWorkObject::Quit(void){
    this->b_isExit = false;
}
//工作对象槽函数,这个里面执行耗时操作
void CKvWorkObject::slot_acquireHiSpedImg(void){
    this->b_isExit = true;//CKvWorkObject的私有成员变量,用于控制线程退出run()函数
    //当主线程中调用requestInterruption,那么这里的QThread::currentThread()->isInterruptionRequested返回的就会是true,也就退出run()函数了
    while(b_isExit && !QThread::currentThread()->isInterruptionRequested()){
        qDebug()<<"Working........";
        Sleep(1000);
    }
}
//==============================================================================



//==========================主UI线程============================================
MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::MainWindow)
{
    ui->setupUi(this);
    
    this->ptr_acquireImgThread = new CKvThread();//线程管理对象
    this->ptr_workObj = new CKvWorkObject();//多线程工作对象
    this->ptr_workObj->moveToThread(this->ptr_acquireImgThread);//改变工作对象的依赖线程
    connect(this , SIGNAL(signal_hiSpdCamCapture()) , this->ptr_workObj , SLOT(slot_acquireHiSpedImg()));//设置工作线程开始耗时工作的触发信号
    connect(this->ptr_acquireImgThread , SIGNAL(finished()) , this->ptr_workObj , SLOT(deleteLater()));//正确退出线程需要连接的信号槽,当线程run()退出时,清理工作对象的子孙对象
    this->ptr_acquireImgThread->start();//开始线程的事件循环


    connect(this->ui->bt_mainWinConnectDev , SIGNAL(clicked(bool)) , this , SLOT(slot_connectDev(void)));//点击Connect按钮工作线程开始运行耗时操作
    connect(this->ui->bt_mainWinDisconnectDev , SIGNAL(clicked(bool) , this , SLOT(slot_disconnectDev(void)));//点击Disconnect按钮工作线程停止耗时操作
}

MainWindow::~MainWindow()
{

    if(NULL != this->ptr_hiSpeedCamera){
        this->ptr_hiSpeedCamera->CamDisconnect();
        this->ptr_hiSpeedCamera->CamUninit();
        delete this->ptr_hiSpeedCamera;
        this->ptr_hiSpeedCamera = NULL;
    }
    //结束线程三部曲,当直接关闭主窗口程序的时候需要如下三步结束子线程才是安全的
    this->ptr_acquireImgThread->requestInterruption();//跳出run中的死循环
    this->ptr_acquireImgThread->quit();//结束线程的事件循环
    this->ptr_acquireImgThread->wait();//等待线程run完全返回再delete
    if(NULL != this->ptr_acquireImgThread){
        delete this->ptr_acquireImgThread;
        this->ptr_acquireImgThread = NULL;
    }
    delete ui;
}

void MainWindow::slot_connectDev(void){
    //如果当前run()函数完成返回了,相当于线程退出了,再次发送消息因为线程的事件循环仍然在运行所以又会重新在线程的run()函数中调用一次slot_acquireHiSpedImg槽函数,相当于重启了线程。但是需要注意的是不能多次发送信号,发送几次信号那么就会调用几次对应的槽函数
    emit signal_hiSpdCamCapture();
}
void MainWindow::slot_disconnectDev(void){
    this->ptr_workObj->Quit();//将工作对象的控制变量设置为false那么run()函数就会退出相当于停止了线程
}

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值