1. 程式人生 > >Qt開發者關於QThread的咆哮——你們都用錯了

Qt開發者關於QThread的咆哮——你們都用錯了

我們(Qt使用者)正廣泛地使用IRC來進行交流。我在Freenode網站掛出了#qt標籤,用於幫助大家解答問題。我經常看到的一個問題(這讓我不厭其煩),是關於理解Qt的執行緒機制以及如何讓他們寫的相關程式碼正確工作。人們貼出他們的程式碼,或者用程式碼寫的範例,而我則總是以這樣的感觸告終:
你們都用錯了!

我覺得有件重要的事情得澄清一下,也許有點唐突了,然而,我不得不指出,下面的這個(假想中的)類是對面向物件原則的錯誤應用,同樣也是對Qt的錯誤應用。

C++
123456789101112131415161718 classMyThread:publicQThread{public:MyThread(){moveToThread(this);}voidrun();signals:voidprogress(int);voiddataReady(QByteArray);publicslots:voiddoWork();voidtimeoutHandler();};

我對這份程式碼最大的質疑在於 moveToThread(this);

 我見過太多人這麼使用,並且完全不明白它做了些什麼。那麼你會問,它究竟做了什麼?moveToThread()函式通知Qt準備好事件處理程式,讓擴充套件的訊號(signal)和槽(slot)在指定執行緒的作用域中呼叫。QThread是執行緒的介面,所以我們是在告訴這個執行緒在“它內部”執行程式碼。我們也應該線上程執行之前做這些事。即使這份程式碼看起來可以執行,但它很混亂,並不是QThread設計中的用法(QThread中寫的所有函式都應該在建立它的執行緒中呼叫,而不是QThread開啟的執行緒)。

在我的印象中,moveToThread(this);  是因為人們在某些文章中看到並且使用而流傳開來的。一次快速的網路搜尋就能找到此類文章,所有這些文章中都有類似如下情形的段落:

  1. 繼承QThread類
  2. 新增用來進行工作的訊號和槽
  3. 測試程式碼,發現槽函式並沒有在“正確的執行緒”中執行
  4. 谷歌一下,發現了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的形式,否則程式編寫會較為複雜。

注:

  1. Thread Affinity:執行緒相關性
  2. “刪除QThread物件前,確保執行緒內所有物件都沒銷燬”一句有誤,應為“被銷燬”,Qt文件中相關記錄為“You must ensure that all objects created in a thread are deleted before you delete the QThread.”