在使用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(); });