日本免费高清视频-国产福利视频导航-黄色在线播放国产-天天操天天操天天操天天操|www.shdianci.com

學無先后,達者為師

網站首頁 編程語言 正文

淺談Qt信號槽與事件循環的關系_C 語言

作者:KumaNPC ? 更新時間: 2022-10-05 編程語言

關于信號槽與事件循環,相關的文章非常多了,本文不做過多介紹。本文主要是通過簡單的幾個例子,嘗試解釋信號槽與事件循環的關系,幫助進一步理解。

一、信號槽

類中聲明的信號,實際也是聲明一個函數,其實現由moc機制自動生成在moc文件里,信號觸發意味著函數調用:

// widget.h , Widget類
signals:
    void widgetSignal1();
// moc_widget.cpp
void Widget::widgetSignal1()
{
    QMetaObject::activate(this, &staticMetaObject, 0, nullptr);
}

Qt中通過QObject::connect建立起信號與信號或槽之間的連接,信號觸發(也即函數調用)時,查找連接信息,從而觸發槽的調用。

QObject::connect,參數可以指定連接類型(Qt::ConnectionType),可以確定槽以什么樣的方式執行。常用自動連接、直接連接、隊列連接。自動連接信號觸發時,根據當前線程與接收者(receiver)所在線程是否相同,選擇直接連接或者隊列連接的執行邏輯。

二、事件循環

很多GUI框架都有事件循環這個概念,借由事件隊列來驅動程序執行不同的邏輯。簡單理解就是,線程內維護一個事件隊列,當事件隊列為空時,線程等待新的事件到來。有事件時,線程取出一個事件,調用該事件對應的處理過程。

UI線程(主線程),通常事件會比較多,例如鼠標鍵盤輸出、重繪等。自定義的線程(QThread實例),也可以啟動一個屬于自己的事件循環,事件多數由程序自己產生。

而Qt的信號槽的機制,一部分也是依賴事件循環實現跨線程執行槽。

三、關系

盡管常說Qt的信號槽依賴事件循環,但實際運用起來,總是出現各種各樣的問題。這里寫幾個使用例子,幫助總結一下。

1. 基本寫法

先做個簡單的測試,在當前線程創建對象并觸發信號:

TestObject * object = new TestObject();
connect(this, SIGNAL(widgetSignal1()), object, SLOT(doTest1()));

qDebug() << "emit in thread: " << QThread::currentThreadId();
emit widgetSignal1();
qDebug() << timer.elapsed();
void TestObject::doTest1()
{
    qDebug() << "doTest1 in thread: " << QThread::currentThreadId();
    QThread::currentThread()->msleep(1000);
}

此時輸出:

emit in thread: ?0x3bd0
doTest1 in thread: ?0x3bd0
1000

如果將connect改為隊列連接:

emit in thread: ?0x1fe0
0
doTest1 in thread: ?0x1fe0

至少可以看出,信號的觸發時的線程與槽執行線程一致,并且默認連接時,似乎等槽執行完成后,才執行后面的代碼。而強制使用隊列隊列連接時,槽的執行被延遲,如果深入研究的話,會發現此時Qt生成了一個QMetaCallEvent事件,事件循環參與其中。

2. 加入額外的線程

這里接涉及不同方式的影響,1. 繼承QThread重寫QThread::run不啟動事件循環;2. moveToThread使用默認事件循環;3. QtConcurrent線程接口和std::thread開啟線程;4.信號觸發者和接收者創建時機; 5.信號觸發時的線程。這幾種情況又相互交錯,非常復雜。

(下面的測試代碼不釋放對象,不考慮內存泄漏,如果某些測試與預期不符,可能是信號多次連接的問題)

繼承QThread,并重寫QThread::run

這是初學者最常用的一種寫法,QThread子類定義信號或者槽,run內觸發信號。此時就涉及到一個非常重要的知識點:對象的所在線程是創建該對象時線程,這也意味著,盡管QThread::run方法是在線程中執行,但QThread對象仍舊是屬于創建它的線程:

MyThread * thread = new MyThread();    // MyThread繼承自QThread
thread->start();
connect(this,SIGNAL(widgetSignal1()), thread, SLOT(doThreadSlot()));
qDebug() << "emit in thread: " << QThread::currentThreadId();
emit widgetSignal1();
qDebug() << timer.elapsed();

輸出:

emit in thread: ?0x52c
doThreadSlot in thread: ?0x52c
2000

此時,觸發的時直接連接的邏輯,輸出跟上面基本寫法里一樣。也可以調用QObject::thread,看看線程id是否與創建時的線程一致。

如果重寫QThread::run方法,在run內觸發MyThread信號:

// Widget類
void Widget::on_pushButton_clicked()
{
    MyThread * thread = new MyThread();
    connect(thread,SIGNAL(progressChanged()), this, SLOT(onProcessChanged()));
    thread->start();
}
// MyThread類
void MyThread::run()
{
    qDebug() << "emit in thread: " << QThread::currentThreadId();
    emit progressChanged();
}

測試輸出,線程不一致。

QThread::run的默認實現時啟動一個事件循環,上面的重寫沒有啟動事件循環。這里就出現了第二個關鍵點:為什么沒有事件循環,信號還是正常觸發了? 當然你可能會懷疑,也許Qt背后偷偷啟動了個呢。

QtConcurrent線程接口和std::thread試試

TestObject * object = new TestObject();
connect(this, SIGNAL(widgetSignal1()), object, SLOT(doTest1()));
QtConcurrent::run([this](){
    qDebug() << "emit in thread: " << QThread::currentThreadId();
    emit widgetSignal1();
});

輸出:

emit in thread: ?0x3088
0
doTest1 in thread: ?0x2ac0

槽正常執行,并且使用了隊列觸發,將QtConcurrent換成std::thread后,也是同樣的結果。因此,信號觸發時,是不需要當前線程有事件循環,因為是通過查找連接信息并根據接收者所在線程來確定是否需要構造事件。

使用moveToThread方式創建線程

moveToThread可以切換指定對象的所屬線程,該方法不是線程安全的,僅允許在對象的所在線程將該對象移動到其他線程。也就是說,將對象從線程A移動到線程B后,可以在線程B里將對象再移動到線程A,但不能在A線程里調用 moveToThread。

文檔里指明,不允許對象父子在不同的線程。moveToThread前,不應該指定對象的parent。

QThread * thread=  new QThread();
TestObject * object = new TestObject();
connect(this, SIGNAL(widgetSignal1()), object, SLOT(doTest1()));
object->moveToThread(thread);
thread->start();    //啟動線程
emit widgetSignal1();    //觸發信號
QTimer::singleShot(1000, this, SIGNAL(widgetSignal1()));
QThread::msleep(10); 
thread->quit();

這段代碼,將TestObject實例object移動到線程,并啟動線程,觸發一次信號,使用QTimer::singleShot延遲1s再次觸發一次信號。最后結束線程事件循環。測試結果顯示,第二次的信號并沒有觸發槽。 因為事件循環提前關閉了。

(休眠10ms是為了避免第一次的信號觸發后,線程事件循環還未開始處理就退出了。如果不休眠10ms,多次執行這段代碼,第一次信號還是有概率觸發槽函數的,這就是線程。)

如果上面的代碼改成:

QThread * thread=  new QThread();
TestObject * object = new TestObject();
connect(this, SIGNAL(widgetSignal1()), object, SLOT(doTest1()));
object->moveToThread(thread);
thread->start();
QTimer::singleShot(1000, this, SIGNAL(widgetSignal1()));
QTimer::singleShot(2000, thread, SLOT(start()));
thread->quit();

多加一句延遲啟動線程,測試結果顯示,第二次的信號觸發的槽成功執行。可見跨線程觸發信號會產生事件并投遞到接收者所在線程隊列。

在不同的線程中創建對象

上面所有的測試代碼都是在主線程創建的對象,主線程事件循環一般情況下總是存在的,如果換成 QtConcurrent 或者 std::thread中創建對象呢?

不用測試也能推測出來,如果接收者所在線程不存在事件循環,那么跨線程的觸發槽不會觸發,因為沒有辦法處理。(但可以在其他線程創建完成后,移動到有事件循環的線程中)。

隊列阻塞連接

(Qt的信號槽連接類型還支持隊列阻塞模式,后面再補充吧)

四、總結

上面的測試,也沒有把所有可能的情況覆蓋。比如再引入QEventLoop可能會出現什么問題。

最后做個簡單的總結,Qt的信號觸發時,根據連接類型、接收者所在線程選擇槽的調用方式。

  • 自動連接,信號觸發時線程 = 接收者所在線程,此時直接調用
  • 自動連接,信號觸發時線程 ≠ 接收者所在線程,產生事件投遞到接收者線程事件循環
  • 如果是隊列連接,產生事件投遞到接收者線程事件循環

也就是,信號的觸發不關心觸發者所在線程有沒有事件循環。只有選擇了隊列方式,產生了事件,才會依賴接收者所在的事件循環處理。因此,信號總是會觸發,如果槽沒有執行,也是接收者的問題。

五、另外一些問題

std::thread和QtConcurrent接口創建的線程差異

一開始我以為信號的觸發也對線程有一定的要求,比如必須是QThread。但實際std::thread內也可以觸發信號。
在這樣的線程中創建對象A,并連接其他線程對象B的信號到A的槽,QtConcurrent可以在線程生存周期內,調用QCoreApplication::processEvents處理對象B觸發的信號,而std::thread沒有這樣的能力。可能QtConcurrent內部是通過QThread實現的,std::thread為什么沒有這樣的能力(畢竟QObject::thread是可以獲取信息的)?

QTimer不能在非QThread線程內啟動,也許也是因為兩者的差異引起的。

QTimer::singleShot啟動0延時,因為不需要真的啟動計時器,不依賴線程的隊列產生超時事件,又都可以用。

原文鏈接:https://blog.csdn.net/eiilpux17/article/details/125345557

欄目分類
最近更新