深度解析qt核心机制:信号槽的多线程行为与对象的线程依附性

对象的线程依附性

每一个学过C++以及系统编程的程序员,对于变量会与特定线程有关联都会感到不可思议;在qt中所说的对象的线程依附性,只是针对继承自QObject的对象而言的;对象的线程依附性,并不是代表真的某个底层线程才能访问这个变量而其他线程不行;而是一种qt实现逻辑上的标记需要;这个qt实现逻辑就是qt核心机制信号槽机制;
qt对象的线程依附性的真正含义是:这个对象只接收或者只处理所依附线程的事件队列里面的事件【有人会问这跟信号槽有什么关系?请先记住这句话!】

在qt中每一个线程都可以有一个唯一的事件队列【类似于windows里面的消息队列】,线程事件队列中接受存放过来的事件任务,这个线程也进行事件循环从事件队列中取出事件任务分派给对应的对象去处理【类似于消息循环分派消息给对应的窗口处理,但是qt中这时分派给对象处理】;注意这里分派给继承自QObject的对象处理;对象所处理的事件任务,一定是从对象所依附的线程的事件队列中取出的任务!

我们现在已经讲了 线程事件队列,线程事件循环,对象的线程依附性;现在来看看connect也就是信号槽的真正语义是什么;
无论采用何种策略,connect的主体语义只有二种
1.调用信号线程在本线程内的直接调用:这时信号的触发或者说调用信号线程与槽函数的触发执行是同一线程;【无论这个emit是手动显示调用还是预定义信号底层通过消息事件触发的,也不管接收者所依附线程是否与调用信号线程是同一线程】对应的emit的语义就是单线程内的直接调用
【注:这种情况下如果emit调用线程与接收者依附线程不是同一个线程,如果此时接收者依附线程处于事件循环的处理过程中而非阻塞过程中,并且刚好是分派给这个接收者对象的某个事件被处理的过程,而direct策略在调用线程上直接执行槽函数;分别在这二个线程上执行处理函数的过程中如果同时读写了这个接收者的相同成员,或者一些共享资源;就会发生数据竞争】
2.调用信号线程存放事件任务到接收者依附线程的事件队列上:这时信号的触发(调用信号)的线程就是存放动作的发出者,由这个线程存放事件任务到接收者所依附线程的事件队列中;所以这时候emit的语义就是事件任务存放到事件队列!
【注:如果此时接收者依附线程与调用信号线程是同一线程,事件依然将被存放到事件队列,并等待事件循环取出事件任务处理;但是这比direct来说是延迟效果】

另外我们再来看这二种策略下引用传参的有效性:
1.如果是直接调用策略:与普通函数调用时的引用传参一样=>依然满足引用传参语义【即槽函数内部对引用变量的写操作,影响到的是emit那边被传递过来的实参,而不存在副本】
2.如果是队列策略:引用语义失效=>即便是引用传参,甚至使用std::ref外附;放入事件队列的任务数据也是复制体

总结: 信号槽引用语义有效性只与是否走队列机制有关!直接调用策略保持引用语义,队列策略引用语义失效!

emit线程接收者依附线程策略实际执行槽函数线程引用语义
GUIGUIdirect与emit同一线程有效
GUIGUIqueue与emit同一线程无效
GUIotherdirect与emit同一线程有效
GUIotherqueue与emit不同线程无效

这里有几个需要注意说明的点:
1.信号触发线程,或者是信号调用线程指的是执行(调用)emit【无论是显示还是隐式】的线程,而非connect 发送者对象所依附的线程!
2.接收者依附线程确实指的是接收者对象所依附的线程

一般而言对象所依附的线程是创建这个对象时【即调用这个对象的构造函数】所在的线程!后面这个对象可以被moveToThread依附到其他线程,但是执行这个操作时需要注意,调用执行这个moveToThread的线程必须是此时这个对象所依附的线程【即依附线程本身才有权决定转让依附权给其他线程】

关于QThread对象的管理线程与所依附线程关系:
QThread对象的管理线程与所依附的线程不是一个线程;QThread对象管理的线程是一个新的底层线程,该线程被QThread对象管理【比如在QThread对象生命周期结束时,必须等待期管理的线程先结束】;
而QThread对象所依附的线程,是定义(创建)QThread对象的线程,可能是GUI线程也可能是其他线程;

connect链接类型参数
Qt::AutoConnection 如果发送信号所在的线程与接受者所依附的线程是同一个线程就是Qt::DirectConnection策略;否则就是Qt::QueuedConnection策略;注【这里所说的发送信号所在的线程是指触发调用 emit 信号的执行线程,并不一定是发送者所依附的线程!】

Qt::DirectConnection 同一线程情况下才会触发此命令;直接立即在同一线程内调用槽函数代码段;发送端此时会被阻塞等待立即调用的完成;原理:最简单的理解成把一段代码“临时插入”到了运行栈;【需要注意可重入性问题】
【注:若信号调用线程与接受者依附线程是不同的线程,但是connect链接强制指定了direct模式,槽函数的执行线程依然是在信号调用线程上,这意味着信号调用的地方会等待槽函数执行结束返回;如果非要谈此时接收者所依附的线程本身处于什么状态,我只能说处于处理事件循环,或者阻塞待处理事件循环的状态】

Qt::QueuedConnection 发送端与接受者所属线程不一样;存放事件到接收者所依附的线程,发送端不阻塞,继续往下执行;接收者等待所属线程的事件循环处理到此派发任务;【若发送端和接受者依附线程一样,强制使用Qt::QueuedConnection方式连接=>这其实是一种延迟行为信号发送线程发送完后继续往下执行,这时槽函数还没被执行,一直到调用信号发送的位置执行完后进入事件循环,处理到刚刚加入的事件后才执行槽函数(处理需要延迟的任务时候用)】

Qt::BlockingQueuedConnection 发送端与接受者所属线程不一样;存放事件到接收者所依附的线程,发送端阻塞等待接收者获得分派的事件任务处理完成后再执行;如果发送端线程与接受者所属线程一样;势必造成死锁行为;

Qt::UniqueConnection 独占链接;多个相同链接调用只成功一个;【相同判定:发送者-信号,接受者-槽都对应相同】
Qt::SingleShotConnection 一次性链接,触发一次槽调用后,这段链接会自动断开;

关于connect链接类型的一些注意事项:
1.Qt::UniqueConnection:当未使用Qt::UniqueConnection指定连接时,多次使用connect( )对同一信号槽建立连接时,这个信号会被触发多次。可以使用Qt::UniqueConnection指定只建立一次连接,这样该信号不会被触发多次,但是Qt::UniqueConnection只对成员函数起作用,不能将它使用到非成员函数的槽以及lambda表达式等。

2.Qt::QueuedConnection:该连接类型只适用于元对象类型。在使用该类型连接之前确保所连接的对象已经注册了元类型以及传递的参数注册了元类型,因为Qt的事件系统需要知道该类型信息,若没有注册元类型该连接不会建立,可以使用qRegisterMetaType()或者Q_DECLARE_METATYPE宏进行元类型注册。

关于信号槽同一个信号链接多个槽函数的执行顺序的新标准(qt5.0之后):
所有这些链接被触发时的最终判定【即根据发送信号所在线程,接收者依附线程,以及链接策略;判定应该在哪个线程上执行槽函数】的结果;被分配在同一个线程上执行的槽函数之间的执行顺序与其connect链接的声明顺序一致;分配在不同线程上执行的槽函数之间执行的顺序不确定!

具体规则如下:

1.采用direct策略之间的槽函数的调用顺序是对应的connect的声明顺序;【无关接收者依附线程】

2.采用queue策略之间的槽函数调用顺序需要看是否落在同一线程上执行:【这里是否会落在同一线程上执行就需要看接收者依附线程来判断了】

	落在同一线程上执行的queue策略的槽函数顺序与声明connect时的顺序一致;
	落在不同线程上执行的queue策略的槽函数的顺序不确定!

3. direct与queue策略之间:

	如果emit调用线程与queue事件队列的事件存放线程为同一线程;那么direct策略对应的槽函数一定先于queue策略对应的槽函数执行
	如果emit调用线程与queue事件队列的事件存放线程不为同一线程;那么direct策略与queue策略对应的槽函数的执行顺序不确定
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include "mythread.h"
#include<QThreadPool>

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
{
    ui->setupUi(this);
    ui->widget_2->setWindowTitle("222");
    ui->widget_2->show();//无效
    ui->textEdit->setText("112");//贯穿widget容器

    // 1. 创建任务对象
    Generate* gen = new Generate(this);
    BubbleSort* bubble = new BubbleSort(this);
    QuickSort* quick = new QuickSort(this);

    //设置线程池线程数量
    QThreadPool::globalInstance()->setMaxThreadCount(3);

    connect(this, &MainWindow::starting, gen, &Generate::recvNum);
    // 2. 启动子线程
    //ui->start的clicked信号是GUI线程调用的,this依附的线程也是GUI线程,所以 emit starting调用是在GUI线程执行的
    connect(ui->start, &QPushButton::clicked, this, [=]()
    {
        emit starting(10000);//因为这个是在GUI线程执行,而gen的所依附线程也是GUI线程,所以这里是在GUI线程直接调用&Generate::recvNum,再调用下面的,故这里也不会出现数据竞争
        QThreadPool::globalInstance()->start(gen);//将gen放入任务队列,待空闲线程取用
    });
    //一个信号链接多个槽,&Generate::sendArray的调用肯定是在另一个线程,而bubble,quick,this对象依附线程是GUI线程,所以这里三个槽函数是会在同一个线程内触发,qt新标准规定这种触发顺序与connect顺序一致
    connect(gen, &Generate::sendArray, bubble, &BubbleSort::recvArray);
    connect(gen, &Generate::sendArray, quick, &QuickSort::recvArray);
    // 接收子线程发送的数据
    connect(gen, &Generate::sendArray, this, [=](QVector<int> list){
        //所以这里上面的recvArray已经触发,甚至是在同一个GUI线程中触发完毕的,这里也不会有数据竞争
        QThreadPool::globalInstance()->start(bubble);
        QThreadPool::globalInstance()->start(quick);
        for(int i=0; i<list.size(); ++i)
        {
            ui->randList->addItem(QString::number(list.at(i)));
        }
    });
    connect(bubble, &BubbleSort::finish, this, [=](QVector<int> list){
        for(int i=0; i<list.size(); ++i)
        {
            ui->bubbleList->addItem(QString::number(list.at(i)));
        }
    });
    connect(quick, &QuickSort::finish, this, [=](QVector<int> list){
        for(int i=0; i<list.size(); ++i)
        {
            ui->quickList->addItem(QString::number(list.at(i)));
        }
    });

    //因为现在gen对象其实是一个task对象而非线程对象;所以gen不需要管理线程,线程由线程池管理;
    //并且 task任务对象设置了setAutoDelete(true);这会在每个任务对象的run方法执行完后自动的去释放task对象;所以也不需要手动delete
//    connect(this, &MainWindow::destroy, this, [=]()
//    {
//        gen->quit();
//        gen->wait();
//        gen->deleteLater();  // 等价与 delete gen;

//        bubble->quit();
//        bubble->wait();
//        bubble->deleteLater();

//        quick->quit();
//        quick->wait();
//        quick->deleteLater();
//    });
}

MainWindow::~MainWindow()
{
    delete ui;
}


  • 25
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Qt中,信号是一种用于对象间通信的机制,可以在多线程环境下使用。通过信号,一个对象可以发射信号,而其他对象可以连接到这个信号并执行相应的函数。 在多线程中使用信号时,需要注意以下几点: 1. 当信号的发送与对象函数的执行在不同线程中时,可能会产生临界资源的竞争问题。因此,需要使用线程间同步机制来保护共享资源的访问。 2. 在Qt中,QThread继承自QObject,因此可以使用发射信号和定义函数的能力。QThread默认声明了几个关键信号: - started():线程开始运行时发射的信号。 - finished():线程完成运行时发射的信号。 - terminated():线程被异常终止时发射的信号。 下面是一个示例代码,演示了在Qt中如何使用信号进行多线程通信: ```cpp #include <QThread> #include <QDebug> // 自定义线程类 class MyThread : public QThread { Q_OBJECT public: void run() override { qDebug() << "Thread started"; // 执行一些耗时操作 // ... // 发射信号 emit mySignal("Hello from thread"); qDebug() << "Thread finished"; } signals: void mySignal(const QString& message); }; // 自定义函数 class MyObject : public QObject { Q_OBJECT public slots: void mySlot(const QString& message) { qDebug() << "Received message:" << message; } }; int main(int argc, char *argv[]) { QCoreApplication a(argc, argv); MyThread thread; MyObject object; // 连接信号 QObject::connect(&thread, SIGNAL(mySignal(QString)), &object, SLOT(mySlot(QString))); // 启动线程 thread.start(); return a.exec(); } ``` 这段代码中,我们创建了一个自定义的线程类`MyThread`,其中重写了`run()`函数,在函数中执行一些耗时操作,并发射了一个自定义的信号`mySignal`。然后,我们创建了一个自定义的对象`MyObject`,其中定义了一个函数`mySlot`,用于接收信号并处理。在`main()`函数中,我们创建了线程对象对象对象,并使用`QObject::connect()`函数将信号连接起来。最后,启动线程并运行Qt事件循环。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值