首頁 > 軟體

QT5 Thread執行緒的具體實現

2022-05-15 16:00:31

QT5 Thread執行緒繼承QThread方式

一.首先分析一下 QTimer Class與 Sleep()函數之間的祕密

QTimer *t = new QTimer(*parent); //建立QTimer 物件

t->start(_time); //計時開始每隔_time時間自動觸發&QTimer::timeout訊號

t->stop(); //結束計時

Sleep() //windows.h裡面的系統延時函數

通過以上方法實現案例:

//button 槽函數
void Widget::on_buttonstart_clicked()
{
    t->start(2000);
    Sleep(3000);  qDebug() << "hello world!"; 
}
//timeout訊號處理常式connect(t, &QTimer::timeout,
            [=]()
    {
        ui->lcd_1->display(++i);
    });

分析,在沒有Sleep()函數的情況下:

點選開始立馬在控制檯顯示hello world!;每隔2秒lcd顯示+1;

有Sleep()的存在後;點選開始程式本質是想每隔2秒lcd顯示+1;3秒後控制檯顯示hello world!;

最終結果是:

點選開始,計時器計時,2秒後,不執行connect();3秒後connect()第一次執行;再過4秒,第二次timeout訊號觸發,再次執行connect();

最終顯示結果為; 過時3秒制臺顯示hello world!lcd顯示 1 再過時1秒顯示2 再過2秒顯示3 依次經過2秒顯示累加1;

二.執行緒的引入;

如果我們想要的結果是,點選按鈕,lcd每一秒顯示+1, 3秒控制檯回顯hello world! 也就是Sleep(3000)顯示hello world!並不會去影響到Qtrimer計時;

單獨建立執行緒A,在A執行緒是實現延時3秒輸出hello world!;

1.一個簡單的控制檯執行緒例子

新建一個qt控制檯程式 自定義一個類 這裡就叫class mythread

//mythread.h#ifndef MYTHREAD_H
#define MYTHREAD_H

#include <QThread>

class myThread: public QThread
{
public:
    myThread();
    void run(); //宣告繼承於QThread虛擬函式 run()
};

#endif // MYTHREAD_H
//mythread.cpp

#include "mythread.h"
#include <QDebug>

myThread::myThread()
{

}
void myThread::run()
{
  qDebug() <<  "hello world!"; //複寫QThread類的 run()函數
}
//main.cpp
#include <QCoreApplication>
#include "mythread.h"   //包涵標頭檔案
int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);

    myThread *thread1 = new myThread; //新建執行緒物件
    thread1->start();  //啟動執行緒
 
    return a.exec();
}

上例啟動了一個新執行緒中輸出hello world!

改進上例:

//mythread.h#ifndef MYTHREAD_H
#define MYTHREAD_H

#include <QThread>

class myThread: public QThread
{
public:
    myThread();
    void run();
    QString name; //新增一個 name 物件
};

#endif // MYTHREAD_H
//mythread.cpp
#include "mythread.h"
#include <QDebug>

myThread::myThread()
{

}
void myThread::run()
{
  qDebug() <<  this->name << "hello world!";
    //新增一個for迴圈
  for(int i = 0; i < 1000; i++)
  {
      qDebug() << this->name << i;
  }
}
//main.cpp

#include <QCoreApplication>
#include "mythread.h"
int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);

    //連續建立三個子執行緒
    myThread *thread1 = new myThread;
    thread1->name = "mythred1";
    thread1->start();
    
    myThread *thread2 = new myThread;
    thread2->name = "mythred2";
    thread2->start();

    myThread *thread3 = new myThread;
    thread3->name = "mythred3";
    thread3->start();

    return a.exec();
}

執行結果:

結果顯示輸出為無序輸出,結論三個執行緒完全獨立執行,互不影響;

2.三個執行緒,自然會有優先權的問題,也就是cpu,先執行哪個執行緒;下面讓我們來談談優先權

執行緒許可權由執行緒啟動函數start(Priority列舉)控制

如上例:在啟動函數中加入枚枚量,具體引數可查幫助檔案:

3.QMutex 類

QMutex類提供了執行緒之間的存取序列化。
QMutex的目的是保護物件,資料結構或程式碼段,以便一次只有一個執行緒可以存取它(這與Java synchronized關鍵字類似)。 QMutexLocker通常最好使用互斥鎖,因為這樣可以很容易地確保鎖定和解鎖一致地執行。

int number = 6;

  void method1()
  {
      number *= 5;
      number /= 4;
  }

  void method2()
  {
      number *= 3;
      number /= 2;
  }

如果執行緒thread1 ,thread2分別順序執行method1(),method2();最終結果將會是:

// method1()
  number *= 5;        // number is now 30
  number /= 4;        // number is now 7

  // method2()
  number *= 3;        // number is now 21
  number /= 2;        // number is now 10

number = 10;

但如果執行緒1在行動時,被系統掛載,或其它種種因素受到延時執行,比如有更高優先順序執行緒申請執行,而執行緒2確並不受影響,最終結果將會是:

// Thread 1 calls method1()
  number *= 5;        // number is now 30

  // Thread 2 calls method2().
  //
  // Most likely Thread 1 has been put to sleep by the operating
  // system to allow Thread 2 to run.
  number *= 3;        // number is now 90
  number /= 2;        // number is now 45

  // Thread 1 finishes executing.
  number /= 4;        // number is now 11, instead of 10

此時number = 11; 並不等於10; 同一程式執行不同結果,這是不允許的

此時就要藉助於QMutex 類;

QMutex mutex;
  int number = 6;

  void method1()
  {
      mutex.lock();
      number *= 5;
      number /= 4;
      mutex.unlock();
  }

  void method2()
  {
      mutex.lock();
      number *= 3;
      number /= 2;
      mutex.unlock();
  }

當你在一個執行緒中呼叫lock()時,其他執行緒會試圖在同一個地方呼叫lock(),直到獲得鎖的執行緒呼叫unlock()。 lock()的一個非阻塞替代是tryLock()。
QMutex在非競爭情況下進行了優化。 如果該互斥體沒有爭用,則非遞迴QMutex將不分配記憶體。 它的構建和銷燬幾乎沒有開銷,這意味著有很多互斥體作為其他類的一部分是很好的。

當執行緒1被cpu延時處理,而執行緒2處理到method2()時自動會進入method1()繼續處理number /=4;再回到method2();而此時如果執行緒1繼續執行時,自動又會進入到method2();

4.QThread 啟動暫停等待訊號與槽控制範例

延續控制檯執行緒例子 在每個執行緒後面加上 thread1->wait(); qDebug() << "hello world!";

預期的結果將會是, 線上程輸出完後才會輸出hello world!

#include <QCoreApplication>
#include "mythread.h"
int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);

    //連續建立三個子執行緒
    myThread *thread1 = new myThread;
    thread1->name = "mythred1";
    thread1->start();
    
    thread1->wait();
    qDebug() << "hello world!";

    return exec();
}

現在轉到GUI下,下面一個例子:

//自定義執行緒類,標頭檔案
#ifndef NITHREAD_H
#define NITHREAD_H

#include <QThread>

class nithread : public QThread
{
    Q_OBJECT
public:
    explicit nithread(QObject *parent = 0);
    bool stop;

signals:
    void sig(int);

protected:
    void run();

public slots:
};

#endif // NITHREAD_H
//自定義執行緒類cpp
#include "nithread.h"
#include <QMutex>
nithread::nithread(QObject *parent) : QThread(parent)
{

}

void nithread::run()
{
    for(int i = 0; i < 100; i++)
    {
        QMutex mutex;
        mutex.lock();
        if(this->stop) break;
        mutex.unlock();
        emit sig(i);
        msleep(100);
    }
}
//GUi視窗類標頭檔案
#ifndef DIALOG_H
#define DIALOG_H

#include <QDialog>
#include <nithread.h>

namespace Ui {
class Dialog;
}

class Dialog : public QDialog
{
    Q_OBJECT

public:
    explicit Dialog(QWidget *parent = 0);
    ~Dialog();

private slots:
    void on_buttonstart_clicked();
    void lot(int);

    void on_buttonstop_clicked();

private:
    Ui::Dialog *ui;
    nithread *threadd;
};

#endif // DIALOG_H
//GUI類cpp
#include "dialog.h"
#include "ui_dialog.h"

Dialog::Dialog(QWidget *parent) :
    QDialog(parent),
    ui(new Ui::Dialog)
{
    ui->setupUi(this);
    threadd = new nithread(this);
    connect(threadd, SIGNAL(sig(int)), this, SLOT(lot(int)));
}

Dialog::~Dialog()
{
    delete ui;
}

void Dialog::on_buttonstart_clicked()
{
    threadd->start();
}

void Dialog::lot(int num)
{
    ui->numberlabel->setText(QString::number(num));
}

void Dialog::on_buttonstop_clicked()
{
    threadd->stop = true;
}
//main.cpp
#include "dialog.h"
#include <QApplication>

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    Dialog w;
    w.show();

    return a.exec();
}

最終結果:

當點選start 開啟執行緒 stop 停止執行緒 通過顯號與槽顯示結果

然而方法一Thread執行緒繼承QThread方式,在實際問題中卻有著很多的問題如下文簡介:早在2006年已經被qt工程師提出;(更直指此方法是錯誤的用法)

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

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

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的形式,否則程式編寫會較為複雜。

從Qt4.4開始,可以採用新的方法也是被稱為正確的方法也是qt想推廣的方法:

// Worker 類定義 cpp #include <QtCore>  
    class Worker : public QObject  
    {  
        Q_OBJECT  
    private slots:  
        void onTimeout()  
        {  
            qDebug()<<"Worker::onTimeout get called from?: "<<QThread::currentThreadId();  
        }  
    };
//main函數cpp

    int main(int argc, char *argv[])  
    {  
        QCoreApplication a(argc, argv);  
        qDebug()<<"From main thread: "<<QThread::currentThreadId();  
       
        QThread t;  
        QTimer timer;  
        Worker worker;  
       
        QObject::connect(&timer, SIGNAL(timeout()), &worker, SLOT(onTimeout()));  
        timer.start(1000);  
       
        worker.moveToThread(&t);  
       
        t.start();  
       
        return a.exec();  
    }

總結:

繼承QThread老式方法

1.定義繼承QThread的類A 複寫run()函數;

2.在主執行緒中範例化A物件a

3.通過呼叫a->start()啟動執行緒,執行緒會自動呼叫run()虛擬函式;run不可直接呼叫;

新方法:

1.建立繼承Obeject的類A 將要線上程中實現的方法在A類中實現

2.在主執行緒中範例化A物件a,再範例化QThread類物件b

3.通過a.moveToThread(&b);將a物件的實現移入執行緒b物件作用範圍內執行

4.b->start()啟動執行緒;

5.通過訊號與槽的方式啟動呼叫A類成員函數;

常用函數:

  • QThread類
  • start(),//啟動執行緒;
  • wait()//等待執行緒執行結束;
  • quit(),//執行緒執行結束退出執行緒

執行緒與程序區別:

程序是系統為每個程式分配有獨立執行空間的執行範例

執行緒是與程序共用記憶體空間的一個獨立執行範例;相對而言執行緒比程序的消耗更低;

結語:

  新版qt5的主要目的也就是讓每個執行緒能獨立執行在其執行緒作用域中,執行緒與執行緒之前的互動則通過connect機制;因此對於需要在子執行緒中使用訊號槽機制的情況,並不推薦使用繼承QThread的形式;些方式僅實用於在只需要在run()中執行一些簡單的函數;

到此這篇關於QT5 Thread執行緒的具體實現的文章就介紹到這了,更多相關QT5 Thread執行緒內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com