淺談Qt信號槽與事件循環(huán)的關(guān)系
關(guān)于信號槽與事件循環(huán),相關(guān)的文章非常多了,本文不做過多介紹。本文主要是通過簡單的幾個例子,嘗試解釋信號槽與事件循環(huán)的關(guān)系,幫助進(jìn)一步理解。
一、信號槽
類中聲明的信號,實際也是聲明一個函數(shù),其實現(xiàn)由moc機制自動生成在moc文件里,信號觸發(fā)意味著函數(shù)調(diào)用:
// widget.h , Widget類 signals: void widgetSignal1();
// moc_widget.cpp void Widget::widgetSignal1() { QMetaObject::activate(this, &staticMetaObject, 0, nullptr); }
Qt中通過QObject::connect建立起信號與信號或槽之間的連接,信號觸發(fā)(也即函數(shù)調(diào)用)時,查找連接信息,從而觸發(fā)槽的調(diào)用。
QObject::connect,參數(shù)可以指定連接類型(Qt::ConnectionType),可以確定槽以什么樣的方式執(zhí)行。常用自動連接、直接連接、隊列連接。自動連接信號觸發(fā)時,根據(jù)當(dāng)前線程與接收者(receiver)所在線程是否相同,選擇直接連接或者隊列連接的執(zhí)行邏輯。
二、事件循環(huán)
很多GUI框架都有事件循環(huán)這個概念,借由事件隊列來驅(qū)動程序執(zhí)行不同的邏輯。簡單理解就是,線程內(nèi)維護(hù)一個事件隊列,當(dāng)事件隊列為空時,線程等待新的事件到來。有事件時,線程取出一個事件,調(diào)用該事件對應(yīng)的處理過程。
UI線程(主線程),通常事件會比較多,例如鼠標(biāo)鍵盤輸出、重繪等。自定義的線程(QThread實例),也可以啟動一個屬于自己的事件循環(huán),事件多數(shù)由程序自己產(chǎn)生。
而Qt的信號槽的機制,一部分也是依賴事件循環(huán)實現(xiàn)跨線程執(zhí)行槽。
三、關(guān)系
盡管常說Qt的信號槽依賴事件循環(huán),但實際運用起來,總是出現(xiàn)各種各樣的問題。這里寫幾個使用例子,幫助總結(jié)一下。
1. 基本寫法
先做個簡單的測試,在當(dāng)前線程創(chuàng)建對象并觸發(fā)信號:
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
至少可以看出,信號的觸發(fā)時的線程與槽執(zhí)行線程一致,并且默認(rèn)連接時,似乎等槽執(zhí)行完成后,才執(zhí)行后面的代碼。而強制使用隊列隊列連接時,槽的執(zhí)行被延遲,如果深入研究的話,會發(fā)現(xiàn)此時Qt生成了一個QMetaCallEvent事件,事件循環(huán)參與其中。
2. 加入額外的線程
這里接涉及不同方式的影響,1. 繼承QThread重寫QThread::run不啟動事件循環(huán);2. moveToThread使用默認(rèn)事件循環(huán);3. QtConcurrent線程接口和std::thread開啟線程;4.信號觸發(fā)者和接收者創(chuàng)建時機; 5.信號觸發(fā)時的線程。這幾種情況又相互交錯,非常復(fù)雜。
(下面的測試代碼不釋放對象,不考慮內(nèi)存泄漏,如果某些測試與預(yù)期不符,可能是信號多次連接的問題)
繼承QThread,并重寫QThread::run
這是初學(xué)者最常用的一種寫法,QThread子類定義信號或者槽,run內(nèi)觸發(fā)信號。此時就涉及到一個非常重要的知識點:對象的所在線程是創(chuàng)建該對象時線程,這也意味著,盡管QThread::run方法是在線程中執(zhí)行,但QThread對象仍舊是屬于創(chuàng)建它的線程:
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
此時,觸發(fā)的時直接連接的邏輯,輸出跟上面基本寫法里一樣。也可以調(diào)用QObject::thread,看看線程id是否與創(chuàng)建時的線程一致。
如果重寫QThread::run方法,在run內(nèi)觸發(fā)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的默認(rèn)實現(xiàn)時啟動一個事件循環(huán),上面的重寫沒有啟動事件循環(huán)。這里就出現(xiàn)了第二個關(guān)鍵點:為什么沒有事件循環(huán),信號還是正常觸發(fā)了? 當(dāng)然你可能會懷疑,也許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
槽正常執(zhí)行,并且使用了隊列觸發(fā),將QtConcurrent換成std::thread后,也是同樣的結(jié)果。因此,信號觸發(fā)時,是不需要當(dāng)前線程有事件循環(huán),因為是通過查找連接信息并根據(jù)接收者所在線程來確定是否需要構(gòu)造事件。
使用moveToThread方式創(chuàng)建線程
moveToThread可以切換指定對象的所屬線程,該方法不是線程安全的,僅允許在對象的所在線程將該對象移動到其他線程。也就是說,將對象從線程A移動到線程B后,可以在線程B里將對象再移動到線程A,但不能在A線程里調(diào)用 moveToThread。
文檔里指明,不允許對象父子在不同的線程。moveToThread前,不應(yīng)該指定對象的parent。
QThread * thread= new QThread(); TestObject * object = new TestObject(); connect(this, SIGNAL(widgetSignal1()), object, SLOT(doTest1())); object->moveToThread(thread); thread->start(); //啟動線程 emit widgetSignal1(); //觸發(fā)信號 QTimer::singleShot(1000, this, SIGNAL(widgetSignal1())); QThread::msleep(10); thread->quit();
這段代碼,將TestObject實例object移動到線程,并啟動線程,觸發(fā)一次信號,使用QTimer::singleShot延遲1s再次觸發(fā)一次信號。最后結(jié)束線程事件循環(huán)。測試結(jié)果顯示,第二次的信號并沒有觸發(fā)槽。 因為事件循環(huán)提前關(guān)閉了。
(休眠10ms是為了避免第一次的信號觸發(fā)后,線程事件循環(huán)還未開始處理就退出了。如果不休眠10ms,多次執(zhí)行這段代碼,第一次信號還是有概率觸發(fā)槽函數(shù)的,這就是線程。)
如果上面的代碼改成:
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();
多加一句延遲啟動線程,測試結(jié)果顯示,第二次的信號觸發(fā)的槽成功執(zhí)行??梢娍缇€程觸發(fā)信號會產(chǎn)生事件并投遞到接收者所在線程隊列。
在不同的線程中創(chuàng)建對象
上面所有的測試代碼都是在主線程創(chuàng)建的對象,主線程事件循環(huán)一般情況下總是存在的,如果換成 QtConcurrent 或者 std::thread中創(chuàng)建對象呢?
不用測試也能推測出來,如果接收者所在線程不存在事件循環(huán),那么跨線程的觸發(fā)槽不會觸發(fā),因為沒有辦法處理。(但可以在其他線程創(chuàng)建完成后,移動到有事件循環(huán)的線程中)。
隊列阻塞連接
(Qt的信號槽連接類型還支持隊列阻塞模式,后面再補充吧)
四、總結(jié)
上面的測試,也沒有把所有可能的情況覆蓋。比如再引入QEventLoop可能會出現(xiàn)什么問題。
最后做個簡單的總結(jié),Qt的信號觸發(fā)時,根據(jù)連接類型、接收者所在線程選擇槽的調(diào)用方式。
- 自動連接,信號觸發(fā)時線程 = 接收者所在線程,此時直接調(diào)用
- 自動連接,信號觸發(fā)時線程 ≠ 接收者所在線程,產(chǎn)生事件投遞到接收者線程事件循環(huán)
- 如果是隊列連接,產(chǎn)生事件投遞到接收者線程事件循環(huán)
也就是,信號的觸發(fā)不關(guān)心觸發(fā)者所在線程有沒有事件循環(huán)。只有選擇了隊列方式,產(chǎn)生了事件,才會依賴接收者所在的事件循環(huán)處理。因此,信號總是會觸發(fā),如果槽沒有執(zhí)行,也是接收者的問題。
五、另外一些問題
std::thread和QtConcurrent接口創(chuàng)建的線程差異
一開始我以為信號的觸發(fā)也對線程有一定的要求,比如必須是QThread。但實際std::thread內(nèi)也可以觸發(fā)信號。
在這樣的線程中創(chuàng)建對象A,并連接其他線程對象B的信號到A的槽,QtConcurrent可以在線程生存周期內(nèi),調(diào)用QCoreApplication::processEvents處理對象B觸發(fā)的信號,而std::thread沒有這樣的能力??赡躋tConcurrent內(nèi)部是通過QThread實現(xiàn)的,std::thread為什么沒有這樣的能力(畢竟QObject::thread是可以獲取信息的)?
QTimer不能在非QThread線程內(nèi)啟動,也許也是因為兩者的差異引起的。
QTimer::singleShot啟動0延時,因為不需要真的啟動計時器,不依賴線程的隊列產(chǎn)生超時事件,又都可以用。
到此這篇關(guān)于淺談Qt信號槽與事件循環(huán)的關(guān)系的文章就介紹到這了,更多相關(guān)Qt信號槽與事件循環(huán)內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
C語言素數(shù)(質(zhì)數(shù))判斷的3種方法舉例
這篇文章主要給大家介紹了關(guān)于C語言素數(shù)(質(zhì)數(shù))判斷的3種方法,質(zhì)數(shù)是只能被1或者自身整除的自然數(shù)(不包括1),稱為質(zhì)數(shù),文中通過代碼介紹的非常詳細(xì),需要的朋友可以參考下2023-11-11