2018年12月11日 星期二

[Qt]分離Ui執行緒與Worker執行緒


有時候為了增進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


分離訊息迴路到不同執行緒,在物件建構程序要注意幾項要點:



用以上概念建構範例,我們設定出兩個角色:

  • 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機制需要考慮生產/消費比例平衡

沒有留言:

張貼留言