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-safe,Synchronize 原型如下:
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 元件方法,反正只要能混過去就好。
這個架構比原來的稍微好一些,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(); }
執行結果:
Override QThread::run() PART 3
//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++ 老兵不死的精神~
我一直期待 bcb + qt 的合體。
回覆刪除合體?
刪除