1. 程式人生 > >QThread使用——關於run和movetoThread的區別

QThread使用——關於run和movetoThread的區別

QThread 使用探討

2010-10-23 00:30

 

注意:本文停止更新,請優先考慮 Qt 執行緒基礎(QThread、QtConcurrent等)

dbzhang800 2011.06.18

QThread 似乎是很難的一個東西,特別是訊號和槽,有非常多的人(儘管使用者本人往往不知道)在用不恰當(甚至錯誤)的方式在使用 QThread,隨便用google一搜,就能搜出大量結果出來。無怪乎Qt的開發人員 Bradley T. Hughes 聲嘶力竭地喊you are-doing-it-wrong

和眾多使用者一樣,初次看到這個時,感到 Bradley T. Hughes有 些莫名奇妙,小題大作。儘管不舒服,當時還是整理過一篇部落格

QThread 的使用方法

時間過去3個月,儘管依然沒怎麼用thread;但今天csdn論壇中有人問到這個問題,想想還是盡我所能整理一下吧。提升自己,方便他人,何樂而不為呢?

QThread東西還是比較多的,而且我對底層物件瞭解有限,僅就一點進行展開(或許是大家最關心的一點):QThread中的slots在那個執行緒中執行?

QThread::run

run 函式是做什麼用的?Manual中說的清楚:

  • run 對於執行緒的作用相當於main函式對於應用程式。它是執行緒的入口,run的開始和結束意味著執行緒的開始和結束。

原文如下(這段話我們稱為定理一吧):

  • The run() implementation is for a thread what the main() entry point is for the application. All code executed in a call stack that starts in the run() function is executed by the new thread, and the thread finishes when the function returns.

這麼短的文字一眼就看完了,可是,這是什麼意思呢?又能說明什麼問題呢?看段簡單程式碼:

class Thread:public QThread 
{ 
    Q_OBJECT 
public: 
    Thread(QObject* parent=0):QThread(parent){} 
public slots: 
    void slot() { ... } 
signals: 
    void sig(); 
protected: 
    void run() { ...} 
}; 
 
int main(int argc, char** argv) 
{ 
... 
    Thread thread; 
... 
}

對照前面的定理,run函式中的程式碼時確定無疑要在次執行緒中執行的,那麼其他的呢?比如 slot 是在次執行緒還是主執行緒中執行?

你想說主執行緒,但又心有不甘,對麼?

QObject::connect

涉及訊號槽,我們就躲不過 connect 函式,只是這個函式大家太熟悉。我不好意思再用一堆廢話來描述它,但不說又不行,那麼折中一下,只看它的最後一個引數吧(為了簡單起見,只看它最常用的3個值)

下面的列表,我們暫稱為定理二:

  • 自動連線(Auto Connection)
    • 這是預設設定
    • 如果訊號在接收者所依附的執行緒內發射,則等同於直接連線
    • 如果發射訊號的執行緒和接受者所依附的執行緒不同,則等同於佇列連線
    • 也就是這說,只存在下面兩種情況
  • 直接連線(Direct Connection)
    • 當訊號發射時,槽函式將直接被呼叫。
    • 無論槽函式所屬物件在哪個執行緒,槽函式都在發射訊號的執行緒內執行。
  • 佇列連線(Queued Connection)
    • 當控制權回到接受者所依附執行緒的事件迴圈時,槽函式被呼叫。
    • 槽函式在接收者所依附執行緒執行。

同前面一樣,這些文字大家都能看懂。但含義呢?

不妨繼續拿前面的例子來看,slot 函式是在主執行緒還是次執行緒中執行呢?

定理二強調兩個概念:傳送訊號的執行緒 和 接收者所依附的執行緒。而 slot 函式屬於我們在main中建立的物件 thread,即thread依附於主執行緒

  • 佇列連線告訴我們:槽函式在接受者所依附執行緒執行。即 slot 將在主執行緒執行
  • 直接連線告訴我們:槽函式在傳送訊號的執行緒執行。訊號在那個執行緒傳送呢??不定!
  • 自動連線告訴我們:二者不同,等同於佇列連線。即 slot 在主執行緒執行

太繞了?不是麼(要徹底理解這幾句話,你可能需要看Qt meta-object系統和Qt event系統)

怎麼辦呢?

如果上兩節看不懂,就記住下面的話吧(自己總結的,用詞上估計會不太準確)。

  • QThread 是用來管理執行緒的,它所依附的執行緒和它管理的執行緒並不是同一個東西
  • QThread 所依附的執行緒,就是執行 QThread t(0) 或 QThread * t=new QThread(0) 的執行緒。也就是咱們這兒的主執行緒
  • QThread 管理的執行緒,就是 run 啟動的執行緒。也就是次執行緒
  • 因為QThread的物件依附在主執行緒中,所以他的slot函式會在主執行緒中執行,而不是次執行緒。除非:
    • QThread 物件依附到次執行緒中(通過movetoThread)
    • slot 和訊號是直接連線,且訊號在次執行緒中發射
  • 但上兩種解決方法都不好,因為QThread不是這麼用的(Bradley T. Hughes)

好了,不再新增更多文字了,看程式碼,估計咱們都會輕鬆點

主執行緒(訊號)QThread(槽)

這是 Qt Manual 和 例子中普遍採用的方法。 但由於manual沒說槽函式是在主執行緒執行的,所以不少人都認為它應該是在次執行緒執行了。

  • 定義一個 Dummy 類,用來發訊號
  • 定義一個 Thread 類,用來接收訊號
    • 過載 run 函式,目的是列印 threadid
/*!
* \file main.cpp
*
* Copyright (C) 2010, dbzhang800
* All rights reserved.
*
*/

#include <QtCore/QCoreApplication> 
#include <QtCore/QObject> 
#include <QtCore/QThread> 
#include <QtCore/QDebug>  
class Dummy:public QObject 
{ 
    Q_OBJECT 
public: 
    Dummy(){} 
public slots: 
    void emitsig() 
    { 
        emit sig(); 
    } 
signals: 
    void sig(); 
}; 
 
class Thread:public QThread 
{ 
    Q_OBJECT 
public: 
    Thread(QObject* parent=0):QThread(parent) 
    { 
        //moveToThread(this); 
    } 
public slots: 
    void slot_main() 
    { 
        qDebug()<<"from thread slot_main:" <<currentThreadId(); 
    } 
protected: 
    void run() 
    { 
        qDebug()<<"thread thread:"<<currentThreadId(); 
        exec(); 
    } 
}; 

#include "main.moc" 

int main(int argc, char *argv[]) 
{  
    QCoreApplication a(argc, argv); 
    qDebug()<<"main thread:"<<QThread::currentThreadId(); 
    Thread thread; 
    Dummy dummy; 
    QObject::connect(&dummy, SIGNAL(sig()), &thread, SLOT(slot_main())); 
    thread.start(); 
    dummy.emitsig(); 
    return a.exec(); 
}

然後看到結果(具體值每次都變,但結論不變)

main thread: 0x1a40 from thread slot_main: 0x1a40 thread thread: 0x1a48

看到了吧,槽函式的執行緒和主執行緒是一樣的!

如果你看過Qt自帶的例子,你會發現 QThread 中 slot 和 run 函式共同操作的物件,都會用QMutex鎖住。為什麼?

因為slot和run處於不同執行緒,需要執行緒間的同步!

如果想讓槽函式slot在次執行緒執行(比如它執行耗時的操作,會讓主執行緒死掉),怎麼解決呢?

  • 注意:dummy訊號是在主執行緒發射的, 接收者 thread 也在主執行緒中。
  • 參考我們前面的結論,很容易想到:
    • 將 thread 依附的執行緒改為次執行緒不就行了?
    • 這也是程式碼中註釋掉的 moveToThread(this)所做的,去掉註釋,你會發現slot在次執行緒中執行

 

main thread: 0x13c0 
thread thread: 0x1de0 
from thread slot_main: 0x1de0

這可以工作,但這是 Bradley T. Hughes 強烈批判的用法。推薦的方法後面會給出。

run中訊號與QThread中槽

  • 定義一個 Dummy 類,在run中發射它的訊號
    • 也可以在run中發射 Thread 類中的訊號,而不是Dummy(效果完全一樣)
  • QThread 定義槽函式,過載run函式
/*!
* \file main.cpp
*
* Copyright (C) 2010, dbzhang800
* All rights reserved.
*
*/

#include <QtCore/QCoreApplication> 
#include <QtCore/QObject> 
#include <QtCore/QThread> 
#include <QtCore/QDebug> 
 
class Dummy:public QObject 
{ 
    Q_OBJECT 
public: 
    Dummy(QObject* parent=0):QObject(parent){} 
public slots: 
    void emitsig() 
    { 
        emit sig(); 
    } 
signals: 
    void sig(); 
}; 
 
class Thread:public QThread 
{ 
    Q_OBJECT 
public: 
    Thread(QObject* parent=0):QThread(parent) 
    { 
        //moveToThread(this); 
    } 
public slots: 
    void slot_thread() 
    { 
        qDebug()<<"from thread slot_thread:" <<currentThreadId(); 
    } 
signals: 
    void sig(); 
protected: 
    void run() 
    { 
        qDebug()<<"thread thread:"<<currentThreadId(); 
        Dummy dummy; 
        connect(&dummy, SIGNAL(sig()), this, SLOT(slot_thread())); 
        dummy.emitsig(); 
        exec(); 
    } 
}; 
 
#include "main.moc" 
 
int main(int argc, char *argv[]) 
{ 
    QCoreApplication a(argc, argv); 
    qDebug()<<"main thread:"<<QThread::currentThreadId(); 
    Thread thread; 
    thread.start(); 
    return a.exec(); 
}

想看結果麼?

main thread: 0x15c0 
thread thread: 0x1750 
from thread slot_thread: 0x15c0
  • 其實沒懸念,肯定是主執行緒
    • thread 物件本身在主執行緒。所以它的槽也在要在主執行緒執行

如何解決呢?

  • (方法一)前面提了 moveToThread,這兒可以用,而且可以解決問題。當同樣,是被批判的物件。
  • (方法二)注意哦,這兒我們的訊號時次執行緒發出的,對比connect連線方式,會發現:
    • 採用直接連線,槽函式將在次執行緒(訊號發出的執行緒)執行
    • 這個方法不太好,因為你需要處理slot和它的物件所線上程的同步。需要 QMutex 一類的東西

推薦的方法

千呼萬喚始出來。

其實,這個方法太簡單,太好用了。定義一個普通的QObject派生類,然後將其物件move到QThread中。使用訊號和槽時根本不用考慮多執行緒的存在。也不用使用QMutex來進行同步,Qt的事件迴圈會自己自動處理好這個。

/*!
* \file main.cpp
*
* Copyright (C) 2010, dbzhang800
* All rights reserved.
*
*/

#include <QtCore/QCoreApplication> 
#include <QtCore/QObject> 
#include <QtCore/QThread> 
#include <QtCore/QDebug> 
 
class Dummy:public QObject 
{ 
    Q_OBJECT 
public: 
    Dummy(QObject* parent=0):QObject(parent)     {} 
public slots: 
    void emitsig() 
    { 
        emit sig(); 
    } 
signals: 
    void sig(); 
}; 
 
class Object:public QObject 
{ 
    Q_OBJECT 
public: 
    Object(){} 
public slots: 
    void slot() 
    { 
        qDebug()<<"from thread slot:" <<QThread::currentThreadId(); 
    } 
}; 
 
#include "main.moc" 
 
int main(int argc, char *argv[]) 
{ 
    QCoreApplication a(argc, argv); 
    qDebug()<<"main thread:"<<QThread::currentThreadId(); 
    QThread thread; 
    Object obj; 
    Dummy dummy; 
    obj.moveToThread(&thread); 
    QObject::connect(&dummy, SIGNAL(sig()), &obj, SLOT(slot())); 
    thread.start(); 
    dummy.emitsig(); 
    return a.exec(); 
}

結果:恩,slot確實不在主執行緒中執行(這麼簡單不值得歡呼麼?)

main thread: 0x1a5c 
from thread slot: 0x186c

其他

  • 本文只考慮了使用事件迴圈的情況,也有可能run中沒有事件迴圈。這時訊號與槽會與本文有點差別。比如run中使用connect時,佇列連線就受限制了。其實只要理解了前面這些,沒有事件迴圈的情況很容易就想通了。

 

 

 

 

 

 

 

 

 

 

 

 

 

Qt 執行緒基礎(QThread、QtConcurrent等)

 

 

使用執行緒

 

基本上有種使用執行緒的場合:

  • 通過利用處理器的多個核使處理速度更快。
  • 為保持GUI執行緒或其他高實時性執行緒的響應,將耗時的操作或阻塞的呼叫移到其他執行緒。

何時使用其他技術替代執行緒

開發人員使用執行緒時需要非常小心。啟動執行緒是很容易的,但確保所有共享資料保持一致很難。遇到問題往往很難解決,這是由於在一段時間內它可能只出現一次或只在特定的硬體配置下出現。在建立執行緒來解決某些問題之前,應該考慮一些替代的技術 :

替代技術

註解

QEventLoop::processEvents()

在一個耗時的計算操作中反覆呼叫QEventLoop::processEvents() 可以防止介面的假死。儘管如此,這個方案可伸縮性並不太好,因為該函式可能會被呼叫地過於頻繁或者不夠頻繁。

QTimer

後臺處理操作有時可以方便地使用Timer安排在一個在未來的某一時刻執行的槽中來完成。在沒有其他事件需要處理時,時間隔為0的定時器超時事件被相應

QSocketNotifier 
QNetworkAccessManager 
QIODevice::readyRead()

這是一個替代技術,替代有一個或多個執行緒在慢速網路執行阻塞讀的情況。只要響應部分的計算可以快速執行,這種設計比線上程中實現的同步等待更好。與執行緒相比這種設計更不容易出錯且更節能(energy efficient)。在許多情況下也有效能優勢。

一般情況下,建議只使用安全和經過測試的方案而避免引入特設執行緒的概念。QtConcurrent 提供了一個將任務分發到處理器所有的核的易用介面。執行緒程式碼完全被隱藏在 QtConcurrent 框架下,所以你不必考慮細節。儘管如此,QtConcurrent 不能用於執行緒執行時需要通訊的情況,而且它也不應該被用來處理阻塞操作。

應該使用 Qt 執行緒的哪種技術?

有時候,你需要的不僅僅是在另一執行緒的上下文中執行一個函式。您可能需要有一個生存在另一個執行緒中的物件來為GUI執行緒提供服務。也許你想在另一個始終執行的執行緒中來輪詢硬體埠並在有關注的事情發生時傳送訊號到GUI執行緒。Qt為開發多執行緒應用程式提供了多種不同的解決方案。解決方案的選擇依賴於新執行緒的目的以及執行緒的生命週期。

生命週期

開發任務

解決方案

一次呼叫

在另一個執行緒中執行一個函式,函式完成時退出執行緒

編寫函式,使用QtConcurrent::run 執行它

派生QRunnable,使用QThreadPool::globalInstance()->start() 執行它

派生QThread,重新實現QThread::run() ,使用QThread::start() 執行它

一次呼叫

需要操作一個容器中所有的項。使用處理器所有可用的核心。一個常見的例子是從影象列表生成縮圖。

QtConcurrent 提供了map()函你數來將操作應用到容器中的每一個元素,提供了fitler()函式來選擇容器元素,以及指定reduce函式作為選項來組合剩餘元素。

一次呼叫

一個耗時執行的操作需要放入另一個執行緒。在處理過程中,狀態資訊需要傳送會GUI執行緒。

使用QThread,重新實現run函式並根據需要傳送訊號。使用訊號槽的queued連線方式將訊號連線到GUI執行緒的槽函式。

持久執行

生存在另一個執行緒中的物件,根據要求需要執行不同的任務。這意味著工作執行緒需要雙向的通訊。

派生一個QObject物件並實現需要的訊號和槽,將物件移動到一個執行有事件迴圈的執行緒中並通過queued方式連線的訊號槽進行通訊。

持久執行

生存在另一個執行緒中的物件,執行諸如輪詢埠等重複的任務並與GUI執行緒通訊。

同上,但是在工作執行緒中使用一個定時器來輪詢。儘管如此,處理輪詢的最好的解決方案是徹底避免它。有時QSocketNotifer是一個替代。

Qt執行緒基礎

QThread是一個非常便利的跨平臺的對平臺原生執行緒的抽象。啟動一個執行緒是很簡單的。讓我們看一個簡短的程式碼:生成一個線上程內輸出"hello"並退出的執行緒。

 // hellothread/hellothread.h
 class HelloThread : public QThread
 {
     Q_OBJECT
 private:
     void run();
 };

我們從QThread派生出一個類,並重新實現run方法。

 // hellothread/hellothread.cpp
 void HelloThread::run()
 {
      qDebug() << "hello from worker thread " << thread()->currentThreadId();
 }

run方法中包含將在另一個執行緒中執行的程式碼。在本例中,一個包含執行緒ID的訊息被打印出來。  QThread::start()將在另一個執行緒中被呼叫。

 int main(int argc, char *argv[])
 {
     QCoreApplication app(argc, argv);
     HelloThread thread;
     thread.start();
     qDebug() << "hello from GUI thread " << app.thread()->currentThreadId();
     thread.wait();  // do not exit before the thread is completed!
     return 0;
 }

QObject與執行緒

QObject有執行緒關聯(thread affinity)[如何翻譯?關聯?依附性?dbzhang800 20110618],換句話說,它生存於一個特定的執行緒。這意味著,在建立時QObject儲存了到當前執行緒的指標。當事件使用postEvent()被派發時,這個資訊變得很有用。事件被放置到相應執行緒的事件迴圈中。如果QObject所依附的執行緒沒有事件迴圈,該事件將永遠不會被傳遞。

要啟動事件迴圈,必須在run()內呼叫exec()。執行緒關聯可以通過moveToThread()來更改。

如上所述,當從其他執行緒呼叫物件的方法時開發人員必須始終保持謹慎。執行緒關聯不會改變這種狀況。 Qt文件中將一些方法標記為執行緒安全。postEvent()就是一個值得注意的例子。一個執行緒安全的方法可以同時在不同的執行緒被呼叫。

通常情況下並不會併發訪問的一些方法,在其他執行緒呼叫物件的非執行緒安全的方法在出現造成意想不到行為的併發訪問前數千次的訪問可能都是工作正常的。編寫測試程式碼不能完全確保執行緒的正確性,但它仍然是重要的。在Linux上,Valgrind和Helgrind有助於檢測執行緒錯誤。

QThread的內部結構非常有趣:

  • QThread並不生存於執行run()的新執行緒內。它生存於舊執行緒中。
  • QThread的大多數成員方法是執行緒的控制介面,並設計成從舊執行緒中被呼叫。不要使用moveToThread()將該介面移動到新建立的執行緒中;呼叫moveToThread(this)被視為不好的實踐。
  • exec()和靜態方法usleep()、msleep()、sleep()要在新建立的執行緒中呼叫。
  • QThread子類中定義的其他成員可在兩個執行緒中訪問。開發人員負責訪問的控制。一個典型的策略是在start()被呼叫前設定成員變數。一旦工作執行緒開始執行,主執行緒不應該操作其他成員。當工作執行緒終止後,主執行緒可以再次訪問其他成員。這是一個線上程開始前傳遞引數並在結束後收集結果的便捷的策略。

QObject必須始終和parent在同一個執行緒。對於在run()中生成的物件這兒有一個驚人的後果:

 void HelloThread::run()
 {
      QObject *object1 = new QObject(this);  //error, parent must be in the same thread
      QObject object2;  // OK
      QSharedPointer <QObject> object3(new QObject); // OK
 }

使用互斥量保護資料的完整

互斥量是一個擁有lock()和unlock()方法並記住它是否已被鎖定的物件。互斥量被設計為從多個執行緒呼叫。如果訊號量未被鎖定lock()將立即返回。下一次從另一個執行緒呼叫會發現該訊號量處於鎖定狀態,然後lock()會阻塞執行緒直到其他執行緒呼叫unlock()。此功能可以確保程式碼段將在同一時間只能由一個執行緒執行。

使用事件迴圈防止資料破壞

Qt的事件迴圈對執行緒間的通訊是一個非常有價值的工具。每個執行緒都可以有它自己的事件迴圈。在另一個執行緒中呼叫一個槽的一個安全的方法是將呼叫放置到另一個執行緒的事件迴圈中。這可以確保目標物件呼叫另一個的成員函式之前可以完成當前正在執行的成員函式。

那麼,如何才能把一個成員呼叫放於一個事件迴圈中? Qt的有兩種方法來做這個。一種方法是通過queued訊號槽連線;另一種是使用QCoreApplication::postEvent()派發一個事件。queued的訊號槽連線是非同步執行的訊號槽連線。內部實現是基於posted的事件。訊號的引數放入事件迴圈後訊號函式的呼叫將立即返回。

連線的槽函式何時被執行依賴於事件迴圈其他的其他操作。

通過事件迴圈通訊消除了我們使用互斥量時所面臨的死鎖問題。這就是我們為什麼推薦使用事件迴圈,而不是使用互斥量鎖定物件的原因。

處理非同步執行

一種獲得一個工作執行緒的結果的方法是等待執行緒終止。在許多情況下,一個阻塞等待是不可接受的。阻塞等待的替代方法是非同步的結果通過posted事件或者queued訊號槽進行傳遞。由於操作的結果不會出現在原始碼的下一行而是在位於原始檔其他部分的一個槽中,這會產生一定的開銷,因為,但在位於原始檔中其他地方的槽。 Qt開發人員習慣於使用這種非同步行為工作,因為它非常相似於GUI程式中使用的的事件驅動程式設計。

----------------------------------------------------------------------------------------------------------------------------------------------------------------分割線

以上是轉自http://blog.csdn.net/chinabinlang/article/details/35988801

下面說一下我的理解。當你使用Qthread在主執行緒中建立執行緒並movetoThread時,那麼run函式中的訊息機制(主要指的是訊號槽機制)是跟隨這個次執行緒(也就是這個qthread物件)。如果你想讓run的訊息機制跑著主執行緒中,那麼你可以繼承QThread並重寫run函式即可。