有時候為了增進Ui的響應速度,不得不把例行工作,尤其那些需要定時呼叫或狀態控制的工作,移到背景執行緒,形成雙執行緒的工作架構(基本上就是自動化產業的101招):
- 主執行緒(Main thread)負責Ui的訊息迴圈,捕捉使用者事件如滑鼠點擊,鍵盤按下等等
- 通常開發框架都幫忙搞定好訊息迴圈架構了,我們只需要填寫各事件的處理常式
- 心得:處理一圈迴圈的延遲能在100ms內搞定,使用者基本上會感覺流暢
- 背景執行緒(Working thread),可能就負責收集外部I/O的資訊,內部物件狀態控制,等等...與介面無關的工作
- 時脈上就與使用者的反應無關,這個執行緒Looping的速度盡量越快越好
- 心得:處理一圈的延遲能在10ms內搞定,順序控制在大部分應用情況不會太糟
考慮Qt框架下,如何做主執行緒與背景執行緒的資料交換?當然前提必須是符合執行緒安全(Thread-safe)與避免競賽狀況(Race condition)
在Qt下,最簡單易懂的達成方法就是Qt的金字招牌,Signal-Slot
Signal-Slot若發送端與接收端屬於同一執行緒時,僅是類似函數指標的連結模式(DirectConnection),發送Signal時即立刻呼叫Slots
而在多執行緒資料交換需求中,機制上會形成類似執行緒安全的佇列(Queue)結構,Qt稱為QueuedConnection,讓時序不同的的接收者執行緒(Receiver)收到Signal後並不是立刻執行,而是堆積在佇列裡,接收者會在他自身的執行緒裡去消化堆積起來的Slots
Queue Connection:
The slot is invoked when control returns to the event loop of the receiver's thread. The slot is executed in the receiver's thread.
另外,背景執行緒不只可以同步執行特定工作,也可以製造另一獨立的訊息迴路去容納各種QObject生存,不用自己另外製造無限迴圈去掃描各物件,讓他們如同在單執行緒的環境下去接收事件(Event)並響應,這個特色讓我們分離執行緒時,僅需要改變建構時的程序
(前提是跨執行緒物件間訊息串接僅使用Signal/Slot)
摘自Qt官網-threads-qobject
分離訊息迴路到不同執行緒,在物件建構程序要注意幾項要點:
- Qt物件"活"在創建它的執行緒當中(超重要)
- 原文:A QObject instance is said to live in the thread in which it is created
- 怎麼決定一Qt物件的訊息迴路落在哪個執行緒?呼叫new QObject()的執行緒就是該Qt物件生存的執行緒
- 建構Qt物件的呼叫端物件,必須要 QObject::moveToThread()
- 原文:The QObject::moveToThread() function changes the thread affinity for an object and its children (the object cannot be moved if it has a parent).
用以上概念建構範例,我們設定出兩個角色:
- myWorker
- 以QTimer在固定時間輸出字串,報出自己所在的執行緒,用來驗證訊息迴路所在,最後發出一Signal通知另一執行緒的Worker
- 製造兩個實例(Instance),一個跑在主執行緒,另一個跑在背景執行緒
void MyObject::onTimeout() { qDebug() << QString("%1,timeout").arg(thread()->objectName()); emit inform(); //!"Cannot send events to objects owned by a different thread //!receiver->setProperty("thread",thread()->objectName()); } void MyObject::onInform() { qDebug() << QString("sender thread %1,inform, on thread %2") .arg(sender()->thread()->objectName()) .arg(thread()->objectName()); }
- myInitiator
- 目的是用來創建myWorker實例
- 按前面所說,創建在背景執行緒的myWorker實例的函式,須在背景執行緒執行;同樣概念套在主執行緒的myWorker實例
- 創建實例用函式Initialize()在執行緒啟動(start)時,透過信號槽觸發執行,能確保myWorker實例都是在背景執行緒中創建
- 另外!myInitiator還需要moveToThread(背景執行緒),否則分離訊息迴路的動作也不會成功(目前不明白為何)
- myWorker間溝通
- 用Signal/Slot達成,執行Slot時報出發送端所在執行緒
- 在主執行緒中完成QObject::connect
//!Called in worker thread void MyInitiator::Initialize() { workerObj = new MyObject(); } //!Called in main thread void MyInitiator::Interconnect() { mainObj = new MyObject(); //! Signal/Slot interconnect QObject::connect(workerObj,&MyObject::inform,mainObj,&MyObject::onInform); QObject::connect(mainObj,&MyObject::inform,workerObj,&MyObject::onInform); }
main.cpp:
int main(int argc, char *argv[]) { QCoreApplication a(argc, argv); QThread* thread = new QThread(); thread->setObjectName("worker"); a.thread()->setObjectName("main"); MyInitiator* initiator = new MyInitiator(); //!Set Thread affinity initiator->moveToThread(thread); //!Call in worker thread after it started QObject::connect(thread,&QThread::started,initiator,&MyInitiator::Initialize); //!Call in main thread QTimer::singleShot(100,[=](){ initiator->Interconnect(); }); //!worker thread event loop start thread->start(); //!main thread event loop start return a.exec(); }
#Github原始碼
後記1:Signal/Slot是我用過的框架裡,深感最棒的執行緒同步機制,簡潔且一致又萬能
待研究:一圈訊息迴路能處理掉多少Queued Slots?畢竟是Queue機制需要考慮生產/消費比例平衡
沒有留言:
張貼留言