1. 程式人生 > >QT執行緒例程之理解

QT執行緒例程之理解

官方原文說明

The QThread class provides a platform-independent way to manage threads.
A QThread object manages one thread of control within the program. QThreads begin executing in run(). By default, run() starts the event loop by calling exec() and runs a Qt event loop inside the thread.

You can use worker objects by moving them to the thread using QObject::moveToThread().
The code inside the Worker’s slot would then execute in a separate thread. However, you are free to connect the Worker’s slots to any signal, from any object, in any thread. It is safe to connect signals and slots across different threads, thanks to a mechanism called queued connections.

Another way to make code run in a separate thread, is to subclass QThread and reimplement run(). For example:程式碼1
In that example, the thread will exit after the run function has returned. There will not be any event loop running in the thread unless you call exec().

我的理解

如下
QThread class是一個與平臺無關的執行緒管理類。在程式中可以使用QThread物件來管理一個執行緒,QThreads物件一啟動後就開始執行.run()

中的程式碼,.run()預設情況下是通過呼叫exec()來開始在該執行緒內部執行QT事件迴圈(注:如果run執行返回了,那麼該執行緒也就結束了)。此後就是介紹兩種執行緒的實現方法了

QT提供了兩種執行緒的實現方法,一種是繼承重定義(subclass),另一種是通過QObject物件的moveToThread()方法實現,qt稱之為worker-object方法

  1. 執行緒繼承重定義

    • 首先,QThreads::run是一個虛方法(virtul),因此你可subclass一個QThread,然後重定義run來實現自己想要執行的程式碼,然後就是對執行緒的啟動,退出之類的操作了。上面說了,執行緒一啟動後就會執行run()
      方法。此處你重定義後就執行你寫的程式碼了,然後傳送resultReady,退出執行緒,如果仍想執行緒繼續執行事件迴圈,你需要呼叫exec()方法。
      這裡要說明下的是:當你將finished訊號與deleteLater關聯起來時,在退出執行緒時會釋放所有執行緒中的物件資源,因此在下次重啟執行緒時你需要再次申請執行緒中的物件,否則就會出現些segment fault之類的錯誤。
    • 程式碼1

        class WorkerThread : public QThread//繼承執行緒類
        {
            Q_OBJECT
            void run() Q_DECL_OVERRIDE {//重定義實現程式碼
                QString result;
                /* ... 執行緒啟動後要執行的執行緒程式碼 */
                emit resultReady(result);
            }
        signals:
            void resultReady(const QString &s);
        };
      
        void MyObject::startWorkInAThread()//主執行緒
        {
            WorkerThread *workerThread = new WorkerThread(this);//例項化一個執行緒
            //執行緒處理後通知主執行緒執行handleResults
            connect(workerThread, &WorkerThread::resultReady, this, &MyObject::handleResults);
            //執行緒結束時自刪物件資源
            connect(workerThread, &WorkerThread::finished, workerThread, &QObject::deleteLater);
            workerThread->start();//開始執行緒
        }
  2. worker-object

    • 自己定義一個worker-object(繼承自QObject),在該物件裡實現自己想要執行的程式碼,然後通過譔物件的moveToThread()方法,將其提交一個QThread物件,此後你只需發射訊號通知worker-object物件執行槽函式,那該槽函式就會自行在QThread管理的分執行緒中執行。特別要說明的是worker-object中的槽函式是在另一個獨立的執行緒中執行的。雖然如此,但worker-object中的槽函式仍然可以與任何執行緒,任何物件中的任何訊號關聯。這也是QT訊號與槽的強大之處。
      重要的事情強調三便以上:槽函式,是以訊號和槽函式的形式才能在另一執行緒上執行,如果直接在主執行緒上以物件方法呼叫的方式,那依然是在主執行緒上執行喲。
      簡言之,你定義可兩個物件A,B,在定義一個執行緒物件C,其中物件A在當前主執行緒上執行,另一給物件B你提交給執行緒物件C去處理,A,B間的訊號與槽可相互關聯,但執行B中的任何槽函式時,都會交由C執行緒去執行之。
      在此處,A,C兩個物件的所處的執行緒都是主執行緒,B物件的所處的執行緒是C執行緒。
      worker-object表述圖
    • 程式碼2

      class Worker : public QObject //工作物件
       {
           Q_OBJECT
      
       public slots:
           void doWork(const QString &parameter) {
               QString result;
               /* ... 想在線上程中實現的程式碼 ... */
               emit resultReady(result);
           }
      
       signals:
           void resultReady(const QString &result);
       };
      
       class Controller : public QObject
       {
           Q_OBJECT
           QThread workerThread;
       public:
           Controller() {
               Worker *worker = new Worker;//例項化一個工作物件
               worker->moveToThread(&workerThread);//提交給分執行緒
               connect(&workerThread, &QThread::finished, worker, &QObject::deleteLater);
               connect(this, &Controller::operate, worker, &Worker::doWork);//關聯不同物件間的訊號與槽
               connect(worker, &Worker::resultReady, this, &Controller::handleResults);
               workerThread.start();//啟動執行緒
           }
           ~Controller() {
               workerThread.quit();//結束執行緒
               workerThread.wait();//等待執行緒結束完畢,沒有這個執行緒退出是快了,但不安全。
           }
       public slots:
           void handleResults(const QString &);
       signals:
           void operate(const QString &);
       };
      
    • 上面的workerThread.start()只是啟動了workerThread執行緒的事件迴圈佇列(引數方法1的描述),並沒有執行worker::doWork,你需要在主執行緒上emit operate才會執行喲。如果還想多幾個函式在分執行緒上執行,你可依此定義Worker的SLOT函式和主執行緒中的訊號關聯就好,當然,在doWork上執行迴圈語句時,下次再emit operate必須等上次的執行結束才有效,同理,要結束這個執行緒那也必須要等doWork迴圈語句執行完才能結束,所以如果用到了迴圈語句,必須要考慮在結束執行緒時如何退出迴圈。
  3. 注意事項:
    繼承重定義方法中,WorkerThread 物件的slot函式,在主執行緒中發出執行訊號後,其執行仍是在例項化它的主執行緒MyObject上執行,而不是你認為的在子執行緒上執行。
    簡單舉例:

          class WorkerThread : public QThread
          {
              Q_OBJECT
              void run() Q_DECL_OVERRIDE {
                  QString result;
                  qDegug()<<"執行run 函式的執行緒號:"<<QThread::currentThreadId(;
                  emit resultReady(result);
              }
          public slot:
              void test(void){
                qDegug()<<"執行slot函式的執行緒號:"<<QThread::currentThread();        
              }
          signals:
              void resultReady(const QString &s);
          };
    
          void MyObject::startWorkInAThread()//主執行緒
          {
              WorkerThread *workerThread = new WorkerThread(this);//例項化一個執行緒
              //執行緒處理後通知主執行緒執行handleResults
              connect(workerThread, &WorkerThread::resultReady, this, &MyObject::handleResults);
              //執行緒結束時自刪物件資源
              connect(workerThread, &WorkerThread::finished, workerThread, &QObject::deleteLater);
              connect(this, SIGNAL(doTest), workerThread, &WorkerThread::test);
              qDegug()<<"main執行緒號:"<<QThread::currentThreadId();
              workerThread->start();//開始執行緒
              emit doTest();          
          }

    執行後可以看出SLOT所線上程是跟main執行緒一致的而不是與RUN所在的分執行緒相同。因為執行run函式時,WorkerThread是自行建立了一個新執行緒去呼叫run的。而其它函式則仍是在建立它的執行緒上呼叫的。QT說明原因如下:

    a QThread instance lives in the old thread that instantiated it, not in the new thread that calls run().This means that all of QThread’s queued slots will execute in the old thread.

    還不是很明白?那再貼一段:

    Like other objects, QThread objects live in the thread where the object was created – not in the thread that is created when QThread::run() is called. It is generally unsafe to provide slots in your QThread subclass, unless you protect the member variables with a mutex.

    上面說的很明白了,QThread objects是存在於建立它的執行緒上,它的所有執行都在該執行緒上,但當呼叫run時,QThread objects會建立一個新的執行緒去執行它。(子類化的QThread的所有資源方法都是在主執行緒上,除非你是在run方法中建立的。)

    worker-object方法中,因為整個worker物件都通過movetoThread轉移到執行緒上了,所以,線上程啟動後,worker物件的所有槽函式的發射執行(呼叫執行不是)都是在分執行緒中執行的。

    記住,當使用執行緒繼承方法時,必須要明白,該繼承的物件的run方法是在新執行緒上執行的同時,該物件的槽函式卻仍是在主執行緒上執行。如果其成員變數在兩類函式上都有使用,必須檢查該變數的安全性(就是變數的在不同執行緒間的同步問題了)。

    故推存使用worker-object

Threads and QObjects總結

  1. 誰建立,誰執行。(object在哪個執行緒上建立,則其函式在哪個執行緒上執行)

    • 不允許在一個執行緒中建立object,而在另一執行緒中呼叫該object的方法
    • 線上程銷燬前必須保證執行緒中的物件都被釋放了。
    • 如果一個執行緒沒有被啟動,那執行緒中的物件的任何方法都無法被呼叫。(如果被呼叫成功,那肯定不在你指望的執行緒中。)
    • 在worker-object中,由於worker-object本身是在主執行緒中建立的,所以依此規則,直接呼叫該物件的槽函式仍在主執行緒中執行,只有emit signal,分執行緒在even loop中接收到該訊息後才執行slot function.
  2. 每個執行緒各自維護一個獨立的event_loop ;

    • event_loop的執行靠呼叫exec();
    • event_loop的退出要呼叫:quit或exit方法
      這裡寫圖片描述
  3. 建立後的物件可通過movetoThread轉移到其它執行緒,斷絕與當前執行緒的關係。

    • QObject 的執行緒會被子物件繼承下來,因此move一個物件時,它的子物件也被move了。
    • 如果一個物件有parent,那該物件無法move,因為其父物件的執行緒與你要移動到的執行緒不一致。QT保證了一個物件的所以子物件同在一個執行緒中。
  4. QThread物件的run函式是在一個單獨的執行緒上執行的,而不是建立QThread物件的執行緒上執行。

    • 由於QThread::run函式和QThread上的slot函式是在不同執行緒上執行的,所以使用QThread 的slot並不安全,可能QThread的成員同時在兩個執行緒上使用造成衝突。
  5. slotsignal的連線方式:

    • Direct Connection
      signal被髮射後,立即在發射signal的執行緒中呼叫slot函式
    • Queued Connection
      signal被髮射後,在控制由接收signal的執行緒的event_loop接管後,呼叫slot。
    • Blocking Queued Connection
      它與上一種的區別是,當前發射執行緒被阻塞,直到slot返回後再次啟用執行。
    • Auto Connection (default)
      當接收方和發射方處同一執行緒,則類似Direct Connection,否則就是Queued Connection,在呼叫connect時,預設使用此引數。
    • Unique Connection
      與自動連線方式相同,但其不能重複執行相同的連線,否則返回false.

應用

參照worker-object的程式碼建立執行緒時,一個還好,多個執行緒就受不了了,每次都要在主執行緒裡寫一堆的start,quit,wait,如果執行緒裡有個迴圈條件,還需要在主執行緒裡將此條件置假才能正常退出執行緒,這樣就相當麻煩,還不如用subclass方式, 為此自己使用如下變種,測試了是可行的。

class Worker : public QObject //物件的建立和銷燬還是在主執行緒執行。
 {
     Q_OBJECT
     bool doLoop;
 public:   
     Worker(){
        moveToThread(&workerThread);
        workerThread.start();
     }
     ~Worker(){
        doLoop = false;
        workerThread.quit();
        workerThread.wait();//為保安全退出一定要加上。
     }     
     QThread workerThread;    
 public slots:
     void doWork(const QString &parameter) {
         QString result;
         doLoop = true;
         while(doLoop&&workerThread.isRunning())
         {
             /* ... 想在線上程中實現的程式碼 ... */
         }
         emit resultReady(result);
     }
     void doWork_other(const QString &parameter); 
 signals:
     void resultReady(const QString &result);
 };


 class Controller : public QObject
 {
     Q_OBJECT     
 public:
     Controller() {
         Worker *worker = new Worker;
         Worker *worker_ohter = new Worker;
         connect(this, &Controller::operate, worker, &Worker::doWork);
         connect(worker, &Worker::resultReady, this, &Controller::handleResults);
         connect(this, &Controller::operate_other, worker_ohter, &Worker::doWork_other);
         connect(worker_ohter, &Worker::resultReady, this, &Controller::handleResults);

     }
     ~Controller()
     {
         delete worker;
         delete worker_other;
     }
 public slots:
     void handleResults(const QString &);
 signals:
     void operate(const QString &);
     void operate_other(const QString &);
 };

這樣子是不是省事很多?主執行緒只管建立物件,connect訊號與槽就好,至於執行緒的結束那些就交管執行緒物件本身去管理好了,這樣不容易弄混結束條件,而且由於執行緒thread是Worker物件的一個成員,因此可以直接用thread.isRuning 等這些執行緒執行情況去判斷,同時,成員執行緒退出時不能去釋放建立它的物件,所以不要再關聯QThread::finished和QObject::deleteLater了。