[QT编程系列-27]:多线程机制 - 信号与槽实现不同对象之间通信的五种情形:线程内同步通信、线程内异步通信、线程间异步通信

目录

理论基础:

0.1信号与槽机制

0.2 线程的事件队列

0.3 线程的信号队列

第1种情形:主线程上下文的两个对象之间同步通信

第2种情形:主线程上下文的两个对象之间异步通信

第3种情形:子线程给主线程上下文中的对象发送异步信号

第4种情形:子线程给move到子线程上下文中的对象发送异步信号

第5种情形:子线程给自身上下文的对象发送异步信号



理论基础:

0.1信号与槽机制

参看前文.....

0.2 线程的事件队列

在Qt中,每个线程都有一个事件队列(event queue),用于存储待处理的事件。

事件队列是线程的主要机制之一,用于实现异步和事件驱动的编程模型。

事件队列中存储的是Qt特定的事件对象,这些事件可以是各种类型,如用户输入事件、定时器事件、自定义事件等。每个事件都有特定的优先级和处理方式。

线程的事件队列基于事件循环(event loop)的概念。一个线程通过运行事件循环来处理事件队列中的事件。事件循环会不断地从事件队列中取出事件,并将其分发给相应的接收者进行处理。当一个事件被处理完毕后,事件循环会继续处理下一个事件,直到事件队列为空或者退出事件循环。

以下是一个简单示例,展示了如何使用事件队列和事件循环:

#include 
#include 
#include 
#include 

class CustomEvent : public QEvent
{
public:
    CustomEvent() : QEvent(QEvent::User) {}
};

class CustomObject : public QObject
{
public:
    bool event(QEvent *event) override
    {
        if (event->type() == QEvent::User) {
            qDebug() << "CustomEvent received and processed";
            return true;
        }
        return QObject::event(event);
    }
};

int main(int argc, char *argv[])
{
    QCoreApplication app(argc, argv);

    CustomEvent customEvent;
    CustomObject customObject;

    // 将自定义事件放入事件队列
    QCoreApplication::postEvent(&customObject, &customEvent);

    // 创建事件循环对象并运行事件循环
    QEventLoop eventLoop;
    eventLoop.exec();

    return app.exec();
}

在这个示例中,我们创建了一个自定义事件CustomEvent,并创建了一个自定义对象CustomObject。然后,我们使用QCoreApplication::postEvent()将自定义事件放入CustomObject所属线程的事件队列。

接下来,我们创建了一个QEventLoop对象eventLoop,并调用其exec()方法开始运行事件循环。在事件循环中,事件队列中的事件会被取出并传递给CustomObject的event()函数进行处理。在本例中,当自定义事件被处理时,会在控制台输出一条消息。

在应用程序的主事件循环中,我们调用了QCoreApplication::exec()来运行应用程序的事件循环。这样,事件队列中的事件才会被处理。

总之,线程的事件队列是用于存储待处理事件的机制,它基于事件循环来实现异步和事件驱动的编程模型。通过将事件放入事件队列,并运行事件循环,线程可以按照事件的顺序进行处理,并响应各种事件。

0.3 线程的信号队列

在Qt中,每个线程都有一个关联的事件循环(event loop)和信号队列(signal queue)

信号队列用于在线程之间进行跨线程的信号传递。

线程的信号队列实现了跨线程异步通信的机制,使得信号发射和槽函数的执行可以在不同线程中进行。当一个信号被发射时,相关的参数会被封装成一个事件,并被放入目标线程信号队列中。目标线程的事件循环会不断从信号队列中取出事件,并将其分发给槽函数进行处理

[QT编程系列-27]:多线程机制 - 信号与槽实现不同对象之间通信的五种情形:线程内同步通信、线程内异步通信、线程间异步通信_第1张图片

以下是一些关于线程信号队列的重要特点和注意事项:

  1. 信号槽连接类型(Connection Type):在连接信号和槽时,需要指定连接类型来决定信号的传递方式。常见的连接类型有:

    • AutoConnection:Qt会根据信号和槽所在的线程自动选择连接类型,如果在不同线程中,则会使用QueuedConnection。
    • DirectConnection:信号立即传递给槽函数,如果信号和槽在不同线程中,则行为未定义。DirectConnection用于发送同步信号,即直接函数调用。
    • QueuedConnection:信号被放入目标线程信号队列中,由目标线程的事件循环处理。QueuedConnection用于发送异步信号,即信号队列间接调用。
  2. 接收者线程的事件循环:目标线程中需要运行事件循环,才能及时处理信号队列中的事件。如果目标线程没有事件循环,信号将会排队等待,直到目标线程启动事件循环。

  3. 线程安全性:Qt中的信号槽机制是线程安全的。当使用QueuedConnection连接信号和槽时,信号被封装为事件对象,并在目标线程的事件队列中进行处理。这样可以避免并发访问的问题,确保信号槽的安全执行。

  4. 避免阻塞和处理延迟:使用QueuedConnection连接信号和槽时,槽函数的执行是异步的,即槽函数执行的时间可以在信号发射后的任何时间点。这意味着槽函数可能会有一定的延迟,并且不会阻塞信号发射者。因此,如果槽函数需要进行耗时的操作,应将其放在后台线程中执行,以避免阻塞主线程或其他重要操作。

需要注意的是,使用线程的信号队列要确保相关的对象已经正确连接了信号槽,并选择合适的连接类型来实现异步跨线程通信。此外,还需要注意线程安全问题,合理处理共享数据的访问和保护。

总而言之,线程的信号队列是Qt中跨线程异步通信的机制,它通过将信号封装为事件对象,将事件放入目标线程的信号队列中,并在目标线程的事件循环中处理,实现了线程间的安全通信。使用信号队列可以避免直接并发访问的问题,使得不同线程中的信号发射和槽函数的执行能够异步进行。

第1种情形:主线程上下文的两个对象之间同步通信

在Qt中,如果在同一线程中存在多个对象,需要进行信号通信和同步,可以使用Qt提供的信号槽机制来实现。

在同一线程内,信号槽的连接是直接的无需使用特定的连接类型,因为信号槽的调用是同步的,不涉及线程切换。

以下是在同一线程内不同对象之间进行同步信号通信的步骤:

  1. 声明信号和槽:在需要发送信号的对象中声明一个信号(用于为关心此信号的对象注册回调函数,即槽函数),以及在接收信号的对象中声明一个相应的槽函数(通过connect,注册到发送信号的对象中)。信号和槽函数可以声明为公共槽或私有槽,具体取决于需求。
class SenderObject : public QObject
{
    Q_OBJECT

signals:
    void sendMessage(const QString& message);
};

class ReceiverObject : public QObject
{
    Q_OBJECT

public slots:
    void onMessageReceived(const QString& message)
    {
        // 处理接收到的消息
    }
};

  1. 连接信号和槽:在对象创建和配置的过程中,将信号与槽函数进行连接,以确保信号发射时能触发相应的槽函数。
int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    MainWindow w;

    qDebug() << QThread::currentThread() << "Main thread started" ;


    SenderObject* sender = new SenderObject;
    ReceiverObject* receiver = new ReceiverObject;

    //QObject::connect(sender, &SenderObject::sendMessage, receiver,         &ReceiverObject::onMessageReceived);

    QObject::connect(sender, &SenderObject::sendMessage, receiver, &ReceiverObject::onMessageReceived, Qt::DirectConnection);

    //QObject::connect(sender, &SenderObject::sendMessage, receiver, &ReceiverObject::onMessageReceived, Qt::QueuedConnection);

  1. 发射信号:在发送信号的对象中,通过调用信号函数并传入相应的参数,发射信号。该信号会直接连接到接收信号的槽函数,并在同一线程中执行槽函数。
   QString message = "Hello, world!";

   qDebug() << QThread::currentThread() << "emit message1" ;
   emit sender->sendMessage(message);
   qDebug() << QThread::currentThread() << "emit message2" ;
   emit sender->sendMessage(message);
   qDebug() << QThread::currentThread() << "emit message done" ;

执行结果:

QThread(0x19177f0) Main windows is running

QThread(0x19177f0) Main thread started

QThread(0x19177f0) emit message1

QThread(0x19177f0) ReceiverObject received message: "Hello, world!"

QThread(0x19177f0) emit message2

QThread(0x19177f0) ReceiverObject received message: "Hello, world!"

QThread(0x19177f0) emit message done

QThread(0x19177f0) Main thread finished

在上述示例中,SenderObject类定义了一个sendMessage信号,ReceiverObject类定义了一个onMessageReceived槽函数。

通过调用connect函数将两个对象的信号和槽进行连接,当SenderObject对象发射sendMessage信号时,ReceiverObject对象的onMessageReceived槽函数将会被调用,并传递相应的参数。

需要注意的是,在同一线程中进行信号通信时,需要确保对象的生命周期和线程的生命周期一致,以免产生悬挂指针问题。

总而言之,Qt的信号槽机制可以在同一线程内的不同对象之间。

第2种情形:主线程上下文的两个对象之间异步通信

 在Qt中,如果在同一线程中存在多个对象,需要进行信号通信和同步,可以使用Qt提供的信号槽机制来实现。

在同一线程内,信号槽的连接可以配置成QueuedConnection,此时,信号槽的调用是异步的,但还是在同一线程内部通信,只不过,需要通过线程的信号队列,而不是直接函数调用。

以下是在同一线程内不同对象之间进行异步信号通信的步骤:

  1. 声明信号和槽:在需要发送信号的对象中声明一个信号(用于为关心此信号的对象注册回调函数,即槽函数),以及在接收信号的对象中声明一个相应的槽函数(通过connect,注册到发送信号的对象中)。信号和槽函数可以声明为公共槽或私有槽,具体取决于需求。
class SenderObject : public QObject
{
    Q_OBJECT

signals:
    void sendMessage(const QString& message);
};

class ReceiverObject : public QObject
{
    Q_OBJECT

public slots:
    void onMessageReceived(const QString& message)
    {
        // 处理接收到的消息
    }
};

  1. 连接信号和槽:在对象创建和配置的过程中,将信号与槽函数进行连接,以确保信号发射时能触发相应的槽函数。
int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    MainWindow w;

    qDebug() << QThread::currentThread() << "Main thread started" ;

SenderObject* sender = new SenderObject;
ReceiverObject* receiver = new ReceiverObject;

//QObject::connect(sender, &SenderObject::sendMessage, receiver, &ReceiverObject::onMessageReceived);

//QObject::connect(sender, &SenderObject::sendMessage, receiver, &ReceiverObject::onMessageReceived, Qt::DirectConnection);

QObject::connect(sender, &SenderObject::sendMessage, receiver, &ReceiverObject::onMessageReceived, Qt::QueuedConnection);

  1. 发射信号:在发送信号的对象中,通过调用信号函数并传入相应的参数,发射信号。该信号会直接连接到接收信号的槽函数,并在同一线程中执行槽函数。
   QString message = "Hello, world!";

   qDebug() << QThread::currentThread() << "emit message1" ;
   emit sender->sendMessage(message);
   qDebug() << QThread::currentThread() << "emit message2" ;
   emit sender->sendMessage(message);
   qDebug() << QThread::currentThread() << "emit message done" ;

执行结果:

QThread(0x1a177f0) Main windows is running

QThread(0x1a177f0) Main thread started

QThread(0x1a177f0) emit message1   //异步发送,不直接调用接收对象的槽函数

QThread(0x1a177f0) emit message2   //异步发送,不直接调用接收对象的槽函数

QThread(0x1a177f0) emit message done

QThread(0x1a177f0) Main thread finished

QThread(0x1a177f0) ReceiverObject received message: "Hello, world!"  //由线程的信号处理函数调用接收对象的槽函数

QThread(0x1a177f0) ReceiverObject received message: "Hello, world!"  //由线程的信号处理函数调用接收对象的槽函数

在上述示例中,SenderObject类定义了一个sendMessage信号,ReceiverObject类定义了一个onMessageReceived槽函数。

通过调用connect函数将两个对象的信号和槽直接连接,当SenderObject对象发射sendMessage信号时,信号首先被缓存到线程的信号队列,并直接返回,ReceiverObject对象的onMessageReceived槽函数将被线程的信号队里查询函数调用,并传递相应的参数,因此会出现异步延时!!!

需要注意的是,在同一线程中进行信号通信时,需要确保对象的生命周期和线程的生命周期一致,以免产生悬挂指针问题。

总而言之,Qt的信号槽机制可以在同一线程内的不同对象之间。

第3种情形:子线程给主线程上下文中的对象发送异步信号

(1)工作类

class MyWorker : public QObject {
    Q_OBJECT

public slots:
    void doWork() {

        qDebug() << QThread::currentThread() << "doWork is called";

        while(0){
            qDebug() << QThread::currentThread() << "Worker thread running" ;
            // 执行耗时操作
            QThread::sleep(3);
        }

        qDebug() << QThread::currentThread() << "doWork is finished";
    }
};

#endif // MYWORKER_H

(2)主线程创建和启动子线程

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    MainWindow w;

    qDebug() << QThread::currentThread() << "Main thread started" ;

#if 1 //子线程调用
    //创建一个管理线程上下文的对象
    QThread* workerThread1 = new QThread;

    //在主线程的上下文context空间,创建一个task对象
    MyWorker* myworker1 = new MyWorker;

    // 将 Worker 对象移到 workerThread 线程
    //myworker1->moveToThread(workerThread1);

    // 把线程的started信号与关心此信号的myworker1对象的槽函数doWork关联起来
    // 该槽函数被哪个线程调用执行,取决于该对象与线程的关系。
    // 1. 对象是由线程A创建的,在线程A的上下文,则该对象的槽函数被线程A执行(本案例)
    // 2. 对象是由线程B创建的,在线程B的上下文,
    //    但通过movetoThread,关联到进程B的上下文,则该对象的槽函数被线程B执行但
    QObject::connect(workerThread1, &QThread::started, myworker1, &MyWorker::doWork, Qt::QueuedConnection);
    //主线程向thread对象发送start信号,启动一个子线程,并执行子线程的run函数
    //run函数会检测QThread的信号队里,并执行信号对应的槽函数。
    //子线程的run启动完成后,子线程对象会发送started信号

    workerThread1->start();
    // 当线程启动时,执行 Worker 的 doWork() 槽函数
    // 线程也是一个对象,因此可以使用对象之间的通信机制,进行通信
    // 子线程启动成功后,子线程立即发送此信号:started
    // myworker1对象的槽函数doWork会被调用

#endif

}

(3)执行结果

QThread(0x19c7770) Main windows is running

QThread(0x19c7770) Main thread started

QThread(0x19c7770) Main thread finished

QThread(0x19c7770) doWork is called     //worker对象的槽函数在主线程的上下文中执行

QThread(0x19c7770) doWork is finished 

第4种情形:子线程给move到子线程上下文中的对象发送异步信号

(1)工作类

class MyWorker : public QObject {
    Q_OBJECT

public slots:
    void doWork() {

        qDebug() << QThread::currentThread() << "doWork is called";

        while(0){
            qDebug() << QThread::currentThread() << "Worker thread running" ;
            // 执行耗时操作
            QThread::sleep(3);
        }

        qDebug() << QThread::currentThread() << "doWork is finished";
    }
};

#endif // MYWORKER_H

(2)主线程创建和启动子线程

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    MainWindow w;

    qDebug() << QThread::currentThread() << "Main thread started" ;

#if 1 //子线程调用
    //创建一个管理线程上下文的对象
    QThread* workerThread1 = new QThread;

    //在主线程的上下文context空间,创建一个task对象
    MyWorker* myworker1 = new MyWorker;

    // 将 Worker 对象移到 workerThread 线程的上下文中
    myworker1->moveToThread(workerThread1);

    // 把线程的started信号与关心此信号的myworker1对象的槽函数doWork关联起来
    // 该槽函数被哪个线程调用执行,取决于该对象与线程的关系。
    // 1. 对象是由线程A创建的,在线程A的上下文,则该对象的槽函数被线程A执行
    // 2. 对象是由线程B创建的,在线程B的上下文,(本案例)
    //    但通过movetoThread,关联到进程B的上下文,则该对象的槽函数被线程B执行但
    QObject::connect(workerThread1, &QThread::started, myworker1, &MyWorker::doWork, Qt::QueuedConnection);
    //主线程向thread对象发送start信号,启动一个子线程,并执行子线程的run函数
    //run函数会检测QThread的信号队里,并执行信号对应的槽函数。
    //子线程的run启动完成后,子线程对象会发送started信号

    workerThread1->start();
    // 当线程启动时,执行 Worker 的 doWork() 槽函数
    // 线程也是一个对象,因此可以使用对象之间的通信机制,进行通信
    // 子线程启动成功后,子线程立即发送此信号:started
    // myworker1对象的槽函数doWork会被调用

#endif

}

(3)执行结果

QThread(0x1b37770) Main windows is running

QThread(0x1b37770) Main thread started

QThread(0x1b37770) Main thread finished

QThread(0x3176f00) doWork is called   //doWork在子线程的上下中得到执行

QThread(0x3176f00) doWork is finished  

4.1 moveToThread原理

moveToThread函数的原理是将一个QObject对象从当前线程移动到目标线程中,并重新分配给目标线程的事件循环进行处理。它的具体原理如下:

  1. moveToThread函数的调用将在目标线程的事件循环中排队一个特殊的事件,该事件会通知Qt框架将对象从当前线程移动到目标线程

  2. 框架会将QObject对象的内部状态进行调整,包括重新设置线程存储(Thread Storage)、调整对象属性和线程局部存储上下文等,以使对象与目标线程关联。

  3. 当事件循环在目标线程中处理这个特殊事件时,Qt会进行一些重要的操作来确保对象的正确移动:

    • 对象内部线程标识会被更改为目标线程的线程标识
    • 对象的父子关系也会被调整,将对象从原来的父对象中移除,并将其设置为目标线程的线程对象的子对象
    • 源线程会释放对该对象的所有权,并且不再处理与该对象相关的事件。
  4. 现在,该QObject对象就完全属于目标线程了,并且可以在目标线程中进行信号的发射和槽函数的调用。

需要注意的是,moveToThread函数只是将QObject对象在线程间的归属关系进行转移,并不会自动将对象的成员函数或信号槽的执行转移到目标线程。因此,在目标线程中调用对象的成员函数或槽函数时,仍然需要与事件循环进行协同,并遵循事件处理的机制和线程安全的要求。

此外,由于QObject对象移动到了新的线程,要确保正确管理和同步对象的访问,以避免潜在的多线程竞争条件和线程安全问题。

4.2 对象的父子关系

在Qt中,可以通过以下几种方式建立对象的父子关系:

  1. 构造函数参数:通过在对象的构造函数中传递父对象指针来建立父子关系。
QObject::QObject(QObject *parent = nullptr)

使用该构造函数创建的对象将自动成为传入的父对象的子对象。

示例:

QObject parentObj;
QObject *childObj = new QObject(&parentObj);

  1. setParent()函数:通过调用对象的setParent()函数来设置父对象。
void QObject::setParent(QObject *parent)

示例:

QObject parentObj;
QObject childObj;
childObj.setParent(&parentObj);

  1. 属性设置:设置对象的QObject::parent属性来建立父子关系。
void QObject::setParent(QObject *parent)

示例:

QObject parentObj;
QObject childObj;
childObj.setProperty("parent", QVariant::fromValue(&parentObj));

无论使用哪种方式建立父子关系,子对象会自动添加到父对象的子对象列表中,并在子对象的析构时自动从父对象中移除。

需要注意的是,在建立父子关系时,要确保父对象的生命周期大于或等于子对象的生命周期,以避免访问已被销毁的父对象或子对象的悬空指针。

此外,父子关系不仅仅是一种简单的组织结构,还会影响事件传播、属性继承和实现一些便利功能,如对象树的遍历和资源管理等。

第5种情形:子线程给自身上下文的对象发送异步信号

如果您不使用moveToThread函数将对象移动到子线程中,而是希望在子线程中调用对象的槽函数,可以使用以下方法:

  1. 使用QThread类:创建一个继承自QThread的子类,重写run函数,在run函数中执行需要在子线程中执行的任务,并在其中调用对象的槽函数。
class WorkerThread : public QThread
{
    Q_OBJECT

public:
    void run() override
    {
        // 在这里执行需要在子线程中执行的任务
        // ...

        // 调用对象的槽函数
        emit mySignal(); // 触发信号,槽函数会在接收线程中被调用
    }

signals:
    void mySignal();
};

然后,在主线程中创建一个WorkerThread对象并启动线程:

WorkerThread thread;
QObject::connect(&thread, SIGNAL(mySignal()), receiver, SLOT(slot()));
thread.start();

  1. 使用QtConcurrent框架:QtConcurrent提供了一种方便的方式在后台线程中执行函数,可以使用QtConcurrent::run来调用对象的成员函数。
QFuture future = QtConcurrent::run(obj, &MyObject::mySlot); // 在后台线程中执行槽函数

需要注意的是,在使用以上方法时,仍需要注意多线程安全性和对象生命周期的管理。确保在子线程中使用的对象在使用结束后正确释放,并避免多个线程同时访问同一资源可能引起的线程安全问题。

总之,使用上述方法,您可以在子线程中调用对象的槽函数,而不必使用moveToThread函数将对象移动到子线程中。

你可能感兴趣的:(编程系列-QT,开发语言,qt,C++)