2016年1月3日 星期日

From C++ Builder to Qt > From TThread to QThread

C++ Builder: TThread

C++ Builder(簡稱 BCB)是十幾年前頗流行的 Windows C++ IDE,現在大概只剩站長這種老人記得。早年若是不肯花苦工去學 MFC 又堅持要用 C++,那剩下的選擇就只有它了。

BCB 易學難精,一開始覺得很容易上手,但直到需求增加、專案規模變大變複雜,馬上就會碰到「撞牆期」,multi-threading 就是一例。


BCB 提供的 thread 元件為 TThread,thread 執行的本體是 TThread::Execute()。然而,隨便一本 BCB 的書或是 BCB 自帶的範例都會告訴你不可以直接呼叫 VCL 元件方法(method),要透過 TThread::Synchronize() 才會 thread-safeSynchronize 原型如下:

typedef void __fastcall (__closure *TThreadMethod)(void);
void __fastcall Synchronize(TThreadMethod &Method);

下面是一個簡單的例子:

#ifndef DEMO_THREAD_H
#define DEMO_THREAD_H

#include <classes.hpp>

namespace Stdctrls{
class TMemo;
}
class TDemoThread : public TThread
{
protected:
    void __fastcall Execute();    
    
public:
    __fastcall TDemoThread(TMemo *memo);
    __fastcall ~TDemoThread();
    
private:
    void __fastcall hello();
    
private:
    int m_count;    
    TMemo *m_memo;    
};    

#endif
#include <stdctrls.hpp>
#include <windows.h>
#include "demo_thread.h"

__fastcall TDemoThread::TDemoThread(TMemo *memo):
    TThread(false),
    m_count(0),
    m_memo(memo)
{
}
//-------------------------------------
__fastcall TDemoThread::~TDemoThread()
{

}    
//-------------------------------------
void __fastcall TDemoThread::Execute()
{
    while(!Terminated)
    {
        Synchronize(hello);
        Sleep(1000); //sleep 1 sec    
    }
}    
//-------------------------------------
void __fastcall TDemoThread::hello()
{    
    AnsiString s;
    s.sprintf("hello %d", m_count);
    m_memo->Lines->Add(s);
    m_count++;
}
//-------------------------------------

啟動/停止 thread:

//-------------------------------------
void __fastcall TForm1::btnStartClick(TObject *Sender)
{
    m_demo_thread = new TDemoThread(Memo1);
}
//-------------------------------------
void __fastcall TForm1::btnStopClick(TObject *Sender)
{
    m_demo_thread->Terminate();
}
//-------------------------------------

執行結果:


各位可以看到,透過 Synchronize 去呼叫 VCL 元件方法的方式非常死板難用沒彈性,光是不能帶參數這點就很惱人,以上面的例子來說不能用 Synchronize("hello") 這樣的方式把參數 pass 給 Memo::Lines::Add()。再說不是每次直接從 TThread::Execute() 直接呼叫 VCL 元件方法都會當掉,所以一堆懶惰的工程師就乾脆直接從 TThread::Execute() 呼叫 VCL 元件方法,反正只要能混過去就好。

對於生性龜毛的站長來說,當然不能忍受這種作法,經過漫長的思考研究,站長終於找出一個 100% thread-safe 的架構:


這個架構比原來的稍微好一些,OS 可以保證 PostMessage() 一定是 thread-safe,main thread 可以沿用原來 event 的處理方式,無論是 MFC 還是 BCB 都允許 user 自行擴充 event handler,如下所示:

#include <stdctrls.hpp>
class TFormMain : public TForm
{
//...............
public:
  __fastcall TFormMain(TComponent* Owner);
BEGIN_MESSAGE_MAP           
  //user defined event handler  
  MESSAGE_HANDLER(WM_RUNAPP, TMessage, Run); 
END_MESSAGE_MAP(TForm);
};

但是仍然沒有解決我們一開始碰到的,從 worker thread pass 參數給 main/GUI thread的問題,你要自己把參數封裝成 PostMessage() 的參數,如果大小超過 wParam, lParam 的 size (PostMessage 的參數)可能要封裝成指標,又引出 memory leak 的可能性,不過這在當時實在是沒有辦法中的辦法了。

Qt: QThread

現在還在 Windows 上打滾的人可以發現一些名人如 Charles Petzold、Jeffrey Richter 等人都改用 C# 來介紹 Windows 程式設計,如果連 Charles Petzold 這位 SDK 元老都改用 C#,C/C++ 的用途(on Windows)大概就只剩 device driver了。

那堅持用 C++ 不想改 C# 的人怎麼辦?又不可能回頭去學 MFC、BCB 這些化石,唯一的出路大概就只剩 Qt 了(外加一個額外的好處可隨時跳槽到 Linux 科科)。而且 Qt 也可以用 VC++ 編譯,這還一併解決過去 BCB C++ 語法支援不如 VC++ 完整、編譯速度太慢的問題(雖然這些 6.0 後續的版本有改善,但已經無法回到過去的榮光了)。這表示絕大多數的 open source 軟件只要有支援 VC++ 編譯起來都不是問題。

接著我們來看看 QThread 有哪幾種用法?跟他的前輩相比他少了哪些缺點?多了哪些優點?有什麼陷阱?

Override QThread::run() PART 1

近乎本能反射地,這大概是從 BCB 陣營轉投 Qt 的工程師會採用的第一個作法,不囉唆我們來看個範例,我們想讓 worker thread 發送 signal(sendMsg) 給 main thread:




//bar.h
#include <QObject>

class Bar : public QObject
{
    Q_OBJECT
public:
    explicit Bar(QObject *parent = 0);
signals:
    void sendMsg(int, QString);
public slots:
    void recvMsg(int, QString);
private:

};
//bar.cpp
#include <QThread>
#include <QDebug>
#include "bar.h"

Bar::Bar(QObject *parent) :
    QObject(parent)
{
}

void Bar::recvMsg(int sender_thread_id, QString msg)
{
    qDebug()<<"Bar received: "
            <<"sender thread id = "
            <<sender_thread_id<<", "
            <<"msg = "<<msg;
}

//foo.h
#ifndef FOO_H
#define FOO_H

#include <QObject>
#include <QString>

class Foo : public QObject
{
    Q_OBJECT
public:
    explicit Foo(QObject *parent = 0);
    void doSendMsg();
signals:
    void sendMsg(int, QString);
public slots:
    void recvMsg(int, QString);

private:


};

#endif // FOO_H

//foo.cpp
#include <QThread>
#include <QDebug>
#include "foo.h"

Foo::Foo(QObject *parent) :
    QObject(parent)
{
}

void Foo::recvMsg(int sender_thread_id, QString msg)
{
    qDebug()<<"Foo received: "
            <<"sender thread id = "
            <<sender_thread_id<<", "
            <<"msg = "<<msg;
}

void Foo::doSendMsg()
{
    emit sendMsg((int)QThread::currentThreadId(), "Hi, I am Foo.");
}

//workthread.h
#ifndef WORKERTHREAD_H
#define WORKERTHREAD_H

#include <QThread>
#include "foo.h"

class WorkerThread : public QThread
{
    Q_OBJECT
public:
    explicit WorkerThread(QObject *parent = 0);
    Foo m_foo;

protected:
    void run();

signals:

public slots:

};

#endif // WORKERTHREAD_H
//workthread.cpp
#include <QDebug>
#include "workerthread.h"

WorkerThread::WorkerThread(QObject *parent) :
    QThread(parent)
{
}

void WorkerThread::run()
{
    qDebug()<<"Worker Thread id = "<<(int)currentThreadId();
    while(1)
    {
        m_foo.doSendMsg();
        msleep(1000); //sleep 1 sec
    }
}


//main.cpp
#include <QtCore/QCoreApplication>
#include <QDebug>
#include "bar.h"
#include "workerthread.h"

int main(int argc, char *argv[])
{
    QCoreApplication app(argc, argv);
    qDebug()<<"Main Thread id = "<<(int)QThread::currentThreadId();
    Bar bar;
    WorkerThread wt;
    QObject::connect(&wt.m_foo, SIGNAL(sendMsg(int,QString)), &bar, SLOT(recvMsg(int,QString)));
    wt.start(); //start worker thread
    return app.exec();
}


執行結果:


結果並沒有什麼意外,我們也看到 Qt 的 signal/slot 機制用起來實在很方便,也沒有醜醜的巨集。

Override QThread::run() PART2


這次我們讓 main thread(Bar) 跟 worker thread(Foo) 互丟訊息看看(worker thread.h/cpp 不變):


程式修改成 Bar 除了收到訊息馬上回傳 signal(sendMsg) 給 Foo,並且在 slot(recvMsg) 內把當下的 thread id 列印出來(Foo 行為相同):

//foo.cpp
void Foo::recvMsg(int sender_thread_id, QString msg)
{
    qDebug()<<"Foo::recvMsg: current thread id ="<<(int)QThread::currentThreadId();
    qDebug()<<"Foo received: sender thread id ="
            <<sender_thread_id<<","
            <<"msg ="<<msg<<endl;
}
//bar.cpp
void Bar::recvMsg(int sender_thread_id, QString msg)
{
    qDebug()<<"Bar::recvMsg: current thread id ="<<(int)QThread::currentThreadId();
    qDebug()<<"Bar received: sender thread id ="
            <<sender_thread_id<<","
            <<"msg ="<<msg<<endl;
    emit sendMsg((int)QThread::currentThreadId(), "Hi, I am Bar.");
}
//main.cpp
int main(int argc, char *argv[])
{
    QCoreApplication app(argc, argv);
    qDebug()<<"Main Thread id = "<<(int)QThread::currentThreadId();
    Bar bar;
    WorkerThread wt;
    QObject::connect(&wt.m_foo, SIGNAL(sendMsg(int,QString)), &bar, SLOT(recvMsg(int,QString)));
    QObject::connect(&bar, SIGNAL(sendMsg(int,QString)), &wt.m_foo, SLOT(recvMsg(int,QString)));
    wt.start(); //start worker thread
    return app.exec();
}

執行結果:


這裡特別把相同的 thread id 標示出來,看出哪裡有問題了嗎?Bar::recvMsg 被呼叫時的 thread id 為 Main Thread id,但 Foo::recvMsg 被呼叫時的 thread id 也是 Main Thread id!?但是他是 WorkerThread 的成員啊?WTF!?

Override QThread::run() PART 3

在瀏覽 QObject 提供的成員函式後,我們發現有個 moveToThread() 可能是我們需要的,我們在 WorkerThread::WorkerThread 增加一行:

//workerthread.cpp
WorkerThread::WorkerThread(QObject *parent):
    QThread(parent)
{
    m_foo.moveToThread(this);
}

執行結果:


這次變成 Foo::recvMsg 收不到 Bar 傳過來的 signal(sendMsg),我們是碰到鬼打牆了嗎?

Override QThread::run() PART4


先公佈答案,再解釋為什麼:
//workerthread.cpp
void WorkerThread::run()

{
    qDebug()<<"Worker Thread id = "<<(int)currentThreadId();
    while(1)
    {        
        m_foo.doSendMsg();
        QCoreApplication::processEvents(); //<---
        msleep(1000); //sleep 1 sec
    }
}

這次結果就對了:


這是因為 QThread 有自己的 event loop:



呼叫 moveToThread() 就是將物件搬移到 QThread 的 event loop,但我們在 run() 裡面又沒有處理 event,於是 Foo::recvMsg() 就永遠都收不到訊息,用 QCoreApplication::processEvents() 就是強迫 Qt 去檢查當前 thread event queue 有沒有未處理的事件?如果有就馬上處理。

不信嗎?我們翻出 QThread::run() 瞧瞧:

//qthread.cpp
void QThread::run()
{
    (void) exec();
}

int QThread::exec()
{
    Q_D(QThread);
    QMutexLocker locker(&d->mutex);
    d->data->quitNow = false;
    if (d->exited) {
        d->exited = false;
        return d->returnCode;
    }
    locker.unlock();

    QEventLoop eventLoop; //<----
    int returnCode = eventLoop.exec(); //<----

    locker.relock();
    d->exited = false;
    d->returnCode = -1;
    return returnCode;
}

看到站長特別標注的那兩行嗎?這下不會懷疑了吧。這也是 Qt 比他的前輩優秀的地方,兩個 thread 有獨立的 event loop、event queue,雙方可以安全的交換訊息,而 Qt 本身也會檢查連接的 signal/slot 是否屬於同一個 thread,如果是就只是簡單的 callback,不是就用 event queue 通訊。如果你用單步直行的方式追蹤 signal 流向,你會看到檢查是否為同一個 thread 的片段:


//qobject.cpp
void QMetaObject::activate(/*...*/)
{
//...............
Qt::HANDLE currentThreadId = QThread::currentThreadId();
//...............
const bool receiverInSameThread = currentThreadId == receiver->d_func()->threadData->threadId;
//...............
}

檢查程式輸出時,發現有些地方不太漂亮:


這是因為 Bar::recvMsg() 與 Foo::recvMsg() 同時被執行,中間發生了 task switch,我們把 Foo 改為跟 Bar 一樣收到 recvMsg() 再回傳就好了:

//workerthread.cpp
void WorkerThread::run()
{
    qDebug()<<"Worker Thread id = "<<(int)currentThreadId();
    m_foo.doSendMsg();
    while(1)
    {                
        QCoreApplication::processEvents();
        msleep(1000); //sleep 1 sec
    }
}
//foo.cpp
void Foo::recvMsg(int sender_thread_id, QString msg)
{
    qDebug()<<"Foo::recvMsg: current thread id ="<<(int)QThread::currentThreadId();
    qDebug()<<"Foo received: sender thread id ="
            <<sender_thread_id<<","
            <<"msg ="<<msg<<endl;
    doSendMsg();
}

void Foo::doSendMsg()
{
    emit sendMsg((int)QThread::currentThreadId(), "Hi, I am Foo.");
}

Without override QThread::run()


從前面的例子我們發現似乎覆寫 QThread::run() 再呼叫 QCoreApplication::processEvents() 不但多此一舉,而且 msleep(1000) 就算改成 msleep(1) 也浪費 CPU (想想 1ms 現代處理器可以做多少事啊!)

下面是我們修改後不覆寫 QThread::run() 的版本:

//workerthread.cpp
WorkerThread::WorkerThread(QObject *parent) :
    QThread(parent)
{    
    m_foo.moveToThread(this);
    connect(this, SIGNAL(started()), this, SLOT(startWork()));
}


void WorkerThread::startWork()
{
    m_foo.doSendMsg();
}

實際執行發現好像跑的太快了,太吃 CPU 了,可以用 QTimer 來調節速度:

class Foo : public QObject
{
    Q_OBJECT
public:
    explicit Foo(QObject *parent = 0);

signals:
    void sendMsg(int, QString);
public slots:
    void recvMsg(int, QString);
    void doSendMsg();
private:
    QTimer m_tmr; //<--
};

Foo::Foo(QObject *parent):
    QObject(parent)
{
    m_tmr.setParent(this); //<--
    m_tmr.setInterval(1000); //<--
    connect(&m_tmr, SIGNAL(timeout()), this, SLOT(doSendMsg()));
}

void Foo::recvMsg(int sender_thread_id, QString msg)
{
    qDebug()<<"Foo::recvMsg: current thread id ="<<(int)QThread::currentThreadId();
    qDebug()<<"Foo received: sender thread id ="
            <<sender_thread_id<<","
            <<"msg ="<<msg<<endl;
    m_tmr.start(); //<--
}

void Foo::doSendMsg()
{
    m_tmr.stop(); //<--
    emit sendMsg((int)QThread::currentThreadId(), "Hi, I am Foo.");
}


尾聲

事實上 Qt 工程師 Bradley T. Hughes 在 2010 年寫過一篇 You’re doing it wrong…,在這篇文章中作者甚至反對用繼承的方式使用 QThread,他認為 QThread 只是個「載具」,除非你確定你要增加 QThread 原本沒有的功能,不然根本沒有繼承的必要,而 Qt 附的範例(examples\tutorials\threads\movedobject\thread.cpp) 就是這種作法,然而實務上有時甚至要回到用 QCoreApplication::processEvents() 的方式,看來 QThread::run() 的原型是 virtual run() 還是有他的存在意義(凡存在必合理?),兩者之間的權衡就看用戶的需求了。不過無論如何都由此可見 Qt 的彈性與強大,C++ 老兵不死的精神~

2 則留言: