Qt開發者關於QThread的咆哮——你們都用錯了
我們(Qt用戶)正廣泛地使用IRC來進行交流。我在Freenode網站掛出了#qt標簽,用於幫助大家解答問題。我經常看到的一個問題(這讓我不厭其煩),是關於理解Qt的線程機制以及如何讓他們寫的相關代碼正確工作。人們貼出他們的代碼,或者用代碼寫的范例,而我則總是以這樣的感觸告終:
你們都用錯了!
我覺得有件重要的事情得澄清一下,也許有點唐突了,然而,我不得不指出,下面的這個(假想中的)類是對面向對象原則的錯誤應用,同樣也是對Qt的錯誤應用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
class MyThread : public QThread
{
public:
MyThread()
{
moveToThread(this);
}
void run();
signals:
void progress(int);
void dataReady(QByteArray);
public slots:
void doWork();
void timeoutHandler();
};
|
我對這份代碼最大的質疑在於 moveToThread(this); 我見過太多人這么使用,並且完全不明白它做了些什么。那么你會問,它究竟做了什么?moveToThread()函數通知Qt准備好事件處理程序,讓擴展的信號(signal)和槽(slot)在指定線程的作用域中調用。QThread是線程的接口,所以我們是在告訴這個線程在“它內部”執行代碼。我們也應該在線程運行之前做這些事。即使這份代碼看起來可以運行,但它很混亂,並不是QThread設計中的用法(QThread中寫的所有函數都應該在創建它的線程中調用,而不是QThread開啟的線程)。
在我的印象中,moveToThread(this); 是因為人們在某些文章中看到並且使用而流傳開來的。一次快速的網絡搜索就能找到此類文章,所有這些文章中都有類似如下情形的段落:
- 繼承QThread類
- 添加用來進行工作的信號和槽
- 測試代碼,發現槽函數並沒有在“正確的線程”中執行
- 谷歌一下,發現了moveToThread(this); 然后寫上“看起來的確管用,所以我加上了這行代碼”
我認為,這些都源於第一步。QThread是被設計來作為一個操作系統線程的接口和控制點,而不是用來寫入你想在線程里執行的代碼的地方。我們(面向對象程序員)編寫子類,是因為我們想擴充或者特化基類中的功能。我唯一想到的繼承QThread類的合理原因,是添加QThread中不包含的功能,比如,也許可以提供一個內存指針來作為線程的堆棧,或者可以添加實時的接口和支持。用於下載文件、查詢數據庫,或者做任何其他操作的代碼都不應該被加入到QThread的子類中;它應該被封裝在它自己的對象中。
通常,你可以簡單地把類從繼承QThread改為繼承QObject,並且,也許得修改下類名。QThread類提供了start()信號,你可以將它連接到你需要的地方來進行初始化操作。為了讓你的代碼實際運行在新線程的作用域中,你需要實例化一個QThread對象,並且使用moveToThread()函數將你的對象分配給它。你同過moveToThread()來告訴Qt將你的代碼運行在特定線程的作用域中,讓線程接口和代碼對象分離。如果需要的話,現在你可以將一個類的多個對象分配到一個線程中,或者將多個類的多個對象分配到一個線程。換句話說,將一個實例與一個線程綁定並不是必須的。
我已經聽到了許多關於編寫Qt多線程代碼時過於復雜的抱怨。原始的QThread類是抽象類,所以必須進行繼承。但到了Qt4.4不再如此,因為QThread::run()有了一個默認的實現。在之前,唯一使用QThread的方式就是繼承。有了線程關聯性的支持,和信號槽連接機制的擴展,我們有了一種更為便利地使用線程的方式。我們喜歡便利,我們想使用它。不幸的是,我太晚地意識到之前迫使人們繼承QThread的做法讓新的方式更難普及。
我也聽到了一些抱怨,是關於沒有同步更新范例程序和文檔來向人們展示如何用最不令人頭疼的方式便利地進行開發的。如今,我能引用的最佳的資源是我數年前寫的一篇博客。
免責聲明:你所看到的上面的一切,當然都只是個人觀點。我在這些類上面花費了很多精力,因此關於要如何使用和不要如何使用它們,我有着相當清晰的想法。
譯者注:
最新的Qt幫助文檔同時提供了建立QThread實例和繼承QThread的兩種多線程實現方式。根據文檔描述和范例代碼來看,若想在子線程中使用信號槽機制,應使用分別建立QThread和對象實例的方式;若只是單純想用子線程運行阻塞式函數,則可繼承QThread並重寫QThread::run()函數。
由於繼承QThread后,必須在QThread::run()函數中顯示調用QThread::exec()來提供對消息循環機制的支持,而QThread::exec()本身會阻塞調用方線程,因此對於需要在子線程中使用信號槽機制的情況,並不推薦使用繼承QThread的形式,否則程序編寫會較為復雜。
擴展閱讀:QObject 之 Thread Affinity
注:
- Thread Affinity:線程相關性
- “刪除QThread對象前,確保線程內所有對象都沒銷毀”一句有誤,應為“被銷毀”,Qt文檔中相關記錄為“You must ensure that all objects created in a thread are deleted before you delete the QThread.”