Qt 子執行緒呼叫connect/QMetaObject::invokeMethod 不呼叫槽函式問題

耿明岩發表於2024-06-09

在使用invokeMethod 進行跨執行緒呼叫的時候,發現invokeMethod在某些情況下不能正常呼叫.

經過查各種資料發現invokeMethod底層的呼叫邏輯是透過Qt事件迴圈處理,所以子執行緒需要顯示的呼叫QEventLoop::exec()或者QCoreApplication::processEvents()執行訊號槽處理.

首先有一個QDemoObject類:

class QDemoObject : public QObject
{
    Q_OBJECT
public:
    explicit QDemoObject(QObject *parent = nullptr);
public slots:
    void printCurrentThrad();
signals:
    void sigPrintCurrentThrad();
};
QDemoObject::QDemoObject(QObject *parent)
    : QObject{parent}
{

}

void QDemoObject::printCurrentThrad()
{
    qDebug() << QThread::currentThreadId();
}

問題舉例

例1

這裡建立了兩個執行緒:

第一個執行緒主要例項化QDemoObject。

假如QDemoObject中printCurrentThrad函式如果想要正常執行,需要保證printCurrentThrad函式一定要在QDemoObject例項化時(new QDemoObject)所線上程執行,也就是printCurrentThrad下的邏輯必須保證線上程1下執行。

為了線上程2中可以正常呼叫printCurrentThrad, 在第二個執行緒中嘗試用invokeMethod非同步方式(Qt::QueuedConnection)呼叫QDemoObject的printCurrentThrad方法:

    QDemoObject* qdemoA = nullptr;
    QtConcurrent::run([&qdemoA]() {//執行緒1
        qdemoA = new QDemoObject;
    });
    QtConcurrent::run([&qdemoA]() {//執行緒2
        bool res = QMetaObject::invokeMethod(qdemoA, "printCurrentThrad", Qt::QueuedConnection);
     if(res) qDebug() << "call ok."; });

執行發現invokeMethod返回值res 為true(列印了call ok.),但是卻沒呼叫到printCurrentThrad函式!

例2

當嘗試把呼叫invokeMethod非同步方式改為同步方式即BlockingQueuedConnection:

    QDemoObject* qdemoA = nullptr;
    QtConcurrent::run([&qdemoA]() {//執行緒1
        qdemoA = new QDemoObject;
    });
    QtConcurrent::run([&qdemoA]() {//執行緒2
        bool res = QMetaObject::invokeMethod(qdemoA, "printCurrentThrad", Qt::BlockingQueuedConnection);
        if(res) qDebug() << "call ok.";
    });

執行發現執行緒2卡在invokeMethod中,永遠沒機會向下執行!

例3

當嘗試把呼叫invokeMethod改為立即執行(DirectConnection)時,正常呼叫printCurrentThrad函式:

    QDemoObject* qdemoA = nullptr;
    QtConcurrent::run([&qdemoA]() {//執行緒1
        qdemoA = new QDemoObject;
    });
    QtConcurrent::run([&qdemoA]() {//執行緒2
        bool res = QMetaObject::invokeMethod(qdemoA, "printCurrentThrad", Qt::DirectConnection);
        if(res) qDebug() << "call ok.";
    });

雖然正常呼叫printCurrentThrad函式,但是printCurrentThrad的邏輯最終是線上程2中處理的,而不是執行緒1。

例4

當嘗試把執行緒1的例項化移出到UI執行緒時,正常呼叫printCurrentThrad:

   //主執行緒(UI執行緒) 
  QDemoObject* qdemoA = nullptr; qdemoA = new QDemoObject; QtConcurrent::run([qdemoA]() {//執行緒2 bool res = QMetaObject::invokeMethod(qdemoA, "printCurrentThrad", Qt::QueuedConnection); if(res) qDebug() << "call ok."; });

正常呼叫printCurrentThrad,並且執行緒ID和GUI執行緒ID一致。

例5

發現在子執行緒中使用connect繫結訊號槽時也有同樣的問題:

    QDemoObject* qdemoA = nullptr;
    QtConcurrent::run([&qdemoA]() {//執行緒1
        qdemoA = new QDemoObject;
        connect(qdemoA, &QDemoObject::sigPrintCurrentThrad, qdemoA, &QDemoObject::printCurrentThrad, Qt::QueuedConnection);
    });
    QtConcurrent::run([&qdemoA]() {//執行緒2
        while(qdemoA == nullptr) QThread::msleep(100);
        emit qdemoA->sigPrintCurrentThrad();
    });

執行緒2傳送訊號後,printCurrentThrad並沒有線上程1中執行.

問題解決

例4可以正常呼叫主要是因為在主執行緒中的某處,一直在反覆呼叫QCoreApplication::processEvents(),Qt程式在main方法中,執行到最後往往呼叫了exec()函式:

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    Dialog w;
    w.show();
    return a.exec();
}

exec()內部可以理解成一個無限死迴圈,反覆呼叫:

while(true) {
    QCoreApplication::processEvents();
}

例1、例2不能正常呼叫printCurrentThrad是因為執行緒1中沒有呼叫QCoreApplication::processEvents();,線上程1中呼叫QCoreApplication::processEvents();後,呼叫正常:

    QDemoObject* qdemoA = nullptr;
    QtConcurrent::run([&qdemoA]() {//執行緒1
        qdemoA = new QDemoObject;
        while(true) {
            //其他邏輯...
            QCoreApplication::processEvents();
            //其他邏輯...
        }
    });
    QtConcurrent::run([&qdemoA]() {//執行緒2
        bool res = QMetaObject::invokeMethod(qdemoA, "printCurrentThrad", Qt::QueuedConnection);
        if(res) qDebug() << "call ok.";
    });

線上程2透過invokeMethod呼叫printCurrentThrad,會發現printCurrentThrad實際執行執行緒是執行緒1.

如果執行緒1中沒有while主迴圈邏輯,也可以透過QEventLoop 的exec執行事件迴圈:

    QDemoObject* qdemoA = nullptr;
    QtConcurrent::run([&qdemoA]() {//執行緒1
        qdemoA = new QDemoObject;
        qDebug() << "thread1:" << QThread::currentThreadId();
        QEventLoop loop;
        loop.exec();
    });
    QtConcurrent::run([&qdemoA]() {//執行緒2
        bool res = QMetaObject::invokeMethod(qdemoA, "printCurrentThrad", Qt::QueuedConnection);
        if(res) qDebug() << "call ok.";
    });

在例5中也是同樣的道理:

    QDemoObject* qdemoA = nullptr;
    QtConcurrent::run([&qdemoA]() {//執行緒1
        qdemoA = new QDemoObject;
        connect(qdemoA, &QDemoObject::sigPrintCurrentThrad, qdemoA, &QDemoObject::printCurrentThrad, Qt::QueuedConnection);
        QEventLoop loop;
        loop.exec();
    });
    QtConcurrent::run([&qdemoA]() {//執行緒2
        while(qdemoA == nullptr) QThread::msleep(100);
        emit qdemoA->sigPrintCurrentThrad();
    });

相關文章