1. 程式人生 > >QThread與多執行緒

QThread與多執行緒

QThread類為我們提供了一種平臺無關的管理執行緒的方式。一個QThread物件管理應用程式中的一個執行緒,該執行緒從run()函式開始執行。並且,預設情況下,我們可以在run()函式中通過呼叫QThread::exec()函式來在當前執行緒中開啟一個事件迴圈。

而使用QThread開啟執行緒的最常用的方式 就是繼承QThread類,重寫其run()方法,因為我們剛才就說過,QThread代表的執行緒就是從run()函式開始執行的。

例如:

  class WorkerThread : public QThread
  {
      Q_OBJECT
      void run() Q_DECL_OVERRIDE {
          QString result;
          /* ... here is the expensive or blocking operation ... */
          emit resultReady(result);
      }
  signals:
      void resultReady(const QString &s);
  };

  void MyObject::startWorkInAThread()
  {
      WorkerThread *workerThread = new WorkerThread(this);
      connect(workerThread, &WorkerThread::resultReady, this, &MyObject::handleResults);
      connect(workerThread, &WorkerThread::finished, workerThread, &QObject::deleteLater);
      workerThread->start();
  }
在這個例子中,該執行緒會在run()函式返回後退出。又因為我們沒有在run()函式中呼叫exec(),所以該執行緒中沒有執行事件迴圈。

另一種使用執行緒的方法,是將要完成的工作封裝到一個工作者物件中,然後使用QObject::moveToThread()函式將該物件移動到一個執行緒物件中。如下:

  class Worker : public QObject
  {
      Q_OBJECT

  public slots:
      void doWork(const QString ¶meter) {
          QString result;
          /* ... here is the expensive or blocking operation ... */
          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 &);
  };
在這個例子中,Worker的槽函式會在一個獨立的執行緒中執行。但是,我們可以自由的將Worker的槽函式連線到任意的訊號上,任意的物件上,任意的執行緒中。並且,由於Qt提供的訊號和槽連線型別中的queued connections型別,使我們可以安全的跨執行緒連線訊號和槽。

並且,很重要的一點是,QThread物件是存活在建立它的那個執行緒中,而不是在執行run()函式的新執行緒中。這意味著,連線到QThread的所有的排隊型槽函式都是在舊執行緒中執行的。因此,如果我們希望我們呼叫的槽函式也在新執行緒中執行,就必須使用這種工作物件的方式。新的槽函式 不應該直接實現在QThread的子類中。

還有,當子類化QThread時,要記住的一點是執行緒物件的建構函式在舊執行緒中執行,而run()在新執行緒中執行。所以,如果在這兩個函式中都訪問了一個成員變數,那麼就是在兩個不同的執行緒中訪問的,要確保訪問的安全性。

下面,我們分別使用這兩種方式,通過列印執行緒id的方法,來看一下它們的區別。
我們先寫一個工作者類,繼承自QObject:

#ifndef WORKER_H
#define WORKER_H

#include <QObject>
#include <QThread>
#include <QDebug>

class Worker : public QObject
{
    Q_OBJECT
public:
    explicit Worker(QObject *parent = 0);

public slots:
    void start();
};

#endif // WORKER_H

#include "worker.h"

Worker::Worker(QObject *parent) : QObject(parent)
{

}

void Worker::start()
{
    qDebug() << "child thread: " << QThread::currentThreadId();
}
我在該類中,只定義了一個start()函式,作為我們執行緒的執行體,其所做的工作也很簡單,只是打印出本執行緒的id。

接下來,再寫一個Controller類,其也繼承自QObject,用來控制執行緒的啟動。

#ifndef CONTROLLER_H
#define CONTROLLER_H

#include <QObject>
#include "worker.h"

class Controller : public QObject
{
    Q_OBJECT
public:
    explicit Controller(QObject *parent = 0);
    void start();

signals:
    void operate();

private:
    QThread workerThread;
};

#endif // CONTROLLER_H

#include "controller.h"

Controller::Controller(QObject *parent) : QObject(parent)
{
    Worker *worker = new Worker;
    worker->moveToThread(&workerThread);
    connect(&workerThread, &QThread::finished, worker, &QObject::deleteLater);
    connect(this, &Controller::operate, worker, &Worker::start);
    workerThread.start();
}

void Controller::start()
{
    emit operate();
}
在這個類中,我們在聲明瞭一個start()函式和一個operate()訊號,還有一個QThread物件。然後在建構函式中,例項化一個Worker類物件,再使用moveToThread()函式,將其移動到我們定義的執行緒物件中;最後,為了啟動我們的執行體,我們將operate()訊號連線到Worker的start()槽函式上。然後,啟動我們的執行緒物件。

main函式如下:

#include <QCoreApplication>
#include "controller.h"

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);

    qDebug() << "main thread: " << QThread::currentThreadId();

    Controller controller;
    controller.start();

    return a.exec();
}
我們定義一個Controller的物件,然後呼叫其start()方法即可。而根據我們在Controller::start()的實現中,發出了operate()訊號,該訊號又連線到Worker::start(),所以會觸發該函式的執行,即我們的執行緒執行體。

其執行結果如下:



可以看出,子執行緒的槽函式確實在一個獨立的執行緒中執行。

而如果我們用繼承QThread的方法來實現該功能,則會看到不同的結果。

先實現Thread類如下:

#ifndef THREAD_H
#define THREAD_H
#include <QThread>
#include <QDebug>

class Thread : public QThread
{
    Q_OBJECT
public:
    Thread(QObject *parent = Q_NULLPTR);

public slots:
    void Come();

protected:
    void run() Q_DECL_OVERRIDE;
};

#endif // THREAD_H

#include "thread.h"

Thread::Thread(QObject *parent)
    :QThread(parent)
{

}

void Thread::Come()
{
    qDebug() << "child thread: " << QThread::currentThreadId();
}

void Thread::run()
{
    exec();
}
我們繼承了QThread類,重新了run()方法,這是繼承QThread開啟執行緒所必須的一步。另外,我們還定義了一個槽函式,Come(),在該函式中列印了本執行緒的id。為了讓執行緒不退出,我們在run()函式中呼叫了QThread::exec()函式,為該執行緒開啟一個事件迴圈,等待某個訊號來觸發Come()槽函式。

接下來,再修改Controller類如下:

#ifndef CONTROLLER_H
#define CONTROLLER_H

#include <QObject>
#include "worker.h"

class Controller : public QObject
{
    Q_OBJECT
public:
    explicit Controller(QObject *parent = 0);
    void start();

signals:
    void operate();
};

#endif // CONTROLLER_H

#include "controller.h"
#include "thread.h"

Controller::Controller(QObject *parent) : QObject(parent)
{
}

void Controller::start()
{
    emit operate();
}
我們只在start()函式中,傳送了operate()訊號。

再修改main函式如下:

#include <QCoreApplication>
#include "controller.h"
#include "thread.h"

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);

    qDebug() << "main thread: " << QThread::currentThreadId();

    Controller controller;
    Thread thread;
    QObject::connect(&controller, SIGNAL(operate()), &thread, SLOT(Come()));
    thread.start();
    controller.start();

    return a.exec();
}

我們先定義了Controller和Thread物件,然後將Controller的operate()訊號連線到Thread的Come()槽函式上,緊接著啟動執行緒,等待訊號。最後呼叫Controller的start()方法,傳送我們定義的訊號,該訊號又會觸發Thread類的槽函式,打印出執行緒ID。

該種方式的執行結果如下:



可見,執行緒類Thread中的槽函式並沒有執行在run()函式所在的新執行緒中,而是和main函式在同一個執行緒中,即建立執行緒物件的執行緒。這有時恐怕不是我們想要的。所以,當需要執行緒中的槽函式完全在另一個新執行緒中執行時,就需要使用moveToThread()的方法。