C++ Qt開發:運用QThread多執行緒元件

lyshark發表於2024-03-06

Qt 是一個跨平臺C++圖形介面開發庫,利用Qt可以快速開發跨平臺窗體應用程式,在Qt中我們可以透過拖拽的方式將不同元件放到指定的位置,實現圖形化開發極大的方便了開發效率,本章將重點介紹如何運用QThread元件實現多執行緒功能。

多執行緒技術在程式開發中尤為常用,Qt框架中提供了QThread庫來實現多執行緒功能。當你需要使用QThread時,需包含QThread模組,以下是QThread類的一些主要成員函式和槽函式。

成員函式/槽函式 描述
QThread(QObject *parent = nullptr) 建構函式,建立一個QThread物件。
~QThread() 解構函式,釋放QThread物件。
void start(QThread::Priority priority = InheritPriority) 啟動執行緒。
void run() 預設的執行緒執行函式,需要在繼承QThread的子類中重新實現以定義執行緒的操作。
void exit(int returnCode = 0) 請求執行緒退出,執行緒將在適當的時候退出。
void quit() 請求執行緒退出,與exit()類似。
void terminate() 立即終止執行緒的執行。這是一個危險的操作,可能導致資源洩漏和未完成的操作。
void wait() 等待執行緒完成。主執行緒將被阻塞,直到該執行緒退出。
bool isRunning() const 檢查執行緒是否正在執行。
void setPriority(Priority priority) 設定執行緒的優先順序。
Priority priority() const 獲取執行緒的優先順序。
QThread::Priority priority() 獲取執行緒的優先順序。
void setStackSize(uint stackSize) 設定執行緒的堆疊大小(以位元組為單位)。
uint stackSize() const 獲取執行緒的堆疊大小。
void msleep(unsigned long msecs) 使執行緒休眠指定的毫秒數。
void sleep(unsigned long secs) 使執行緒休眠指定的秒數。
static QThread *currentThread() 獲取當前正在執行的執行緒的QThread物件。
void setObjectName(const QString &name) 為執行緒設定一個物件名。

當我們需要建立執行緒時,通常第一步則是要繼承QThread類,並重寫類內的run()方法,在run()方法中,你可以編寫需要在新執行緒中執行的程式碼。當你建立一個QThread的例項並呼叫它的start()方法時,會自動呼叫run()來執行執行緒邏輯,如下這樣一段程式碼展示瞭如何運用執行緒類。

#include <QCoreApplication>
#include <QThread>
#include <QDebug>

class MyThread : public QThread
{
public:
    void run() override
    {
        for (int i = 0; i < 5; ++i)
        {
            qDebug() << "Thread is running" << i;
            sleep(1);
        }
    }
};

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

    MyThread thread;
    thread.start();
    thread.wait();

    qDebug() << "Main thread is done.";
    return a.exec();
}

上述程式碼執行後則會每隔1秒輸出一段話,在主函式內透過呼叫thread.start方法啟動這個執行緒,並透過thread.wait等待執行緒結束,如下圖所示;

1.1 執行緒組與多執行緒

執行緒組是一種組織和管理多個執行緒的機制,允許將相關聯的執行緒集中在一起,便於集中管理、協調和監控。透過執行緒組,可以對一組執行緒進行統一的生命週期管理,包括啟動、停止、排程和資源分配等操作。

上述方法並未真正實現多執行緒功能,我們繼續完善MyThread自定義類,在該類內增加兩個標誌,is_run()用於判斷執行緒是否正在執行,is_finish()則用來判斷執行緒是否已經完成,並在run()中增加列印當前執行緒物件名稱的功能。

class MyThread: public QThread
{
protected:
    volatile bool m_to_stop;

protected:
    void run()
    {
        for(int x=0; !m_to_stop && (x <10); x++)
        {
            msleep(1000);
            std::cout << objectName().toStdString() << std::endl;
        }
    }

public:
    MyThread()
    {
        m_to_stop = false;
    }

    void stop()
    {
        m_to_stop = true;
    }

    void is_run()
    {
        std::cout << "Thread Running = " << isRunning() << std::endl;
    }

    void is_finish()
    {
        std::cout << "Thread Finished = " << isFinished() << std::endl;
    }

};

接著在主函式內調整,增加一個MyThread thread[10]用於儲存執行緒組,執行緒組是一種用於組織和管理多個執行緒的概念。在不同的程式設計框架和作業系統中,執行緒組可能具有不同的實現和功能,但通常用於提供一種集中管理和協調一組相關執行緒的機制。

我們透過迴圈的方式依次對執行緒組進行賦值,透過呼叫setObjectName對每一個執行緒賦予一個不同的名稱,當需要使用這些執行緒時則可以透過迴圈呼叫run()方法來實現,而結束呼叫同樣如此,如下是呼叫的具體實現;

#include <QCoreApplication>
#include <iostream>
#include <QThread>

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

    // 定義執行緒陣列
    MyThread thread[10];

    // 設定執行緒物件名字
    for(int x=0;x<10;x++)
    {
        thread[x].setObjectName(QString("thread => %1").arg(x));
    }

    // 批次呼叫run執行
    for(int x=0;x<10;x++)
    {
        thread[x].start();
        thread[x].is_run();
        thread[x].isFinished();
    }

    // 批次呼叫stop關閉
    for(int x=0;x<10;x++)
    {
        thread[x].wait();
        thread[x].stop();

        thread[x].is_run();
        thread[x].is_finish();
    }

    return a.exec();
}

如下圖則是執行後實現的多執行緒效果;

1.2 向執行緒中傳遞引數

向執行緒中傳遞引數是多執行緒程式設計中常見的需求,不同的程式語言和框架提供了多種方式來實現這個目標,在Qt中,由於使用的自定義執行緒類,所以可透過增加一個set_value()方法來向執行緒內傳遞引數,由於執行緒函式內的變數使用了protected屬性,所以也就實現了執行緒間變數的隔離,當執行緒被執行結束後則可以透過result()方法獲取到執行緒執行結果,這個執行緒函式如下所示;

class MyThread: public QThread
{
protected:
    int m_begin;
    int m_end;
    int m_result;

    void run()
    {
        m_result = m_begin + m_end;
    }

public:
    MyThread()
    {
        m_begin = 0;
        m_end = 0;
        m_result = 0;
    }

    // 設定引數給當前執行緒
    void set_value(int x,int y)
    {
        m_begin = x;
        m_end = y;
    }

    // 獲取當前執行緒名
    void get_object_name()
    {
        std::cout << "this thread name => " << objectName().toStdString() << std::endl;
    }

    // 獲取執行緒返回結果
    int result()
    {
        return m_result;
    }
};

在主函式中,我們透過MyThread thread[3];來定義3個執行緒組,並透過迴圈三次分別thread[x].set_value()設定三組不同的引數,當設定完成後則可以呼叫thread[x].start()方法執行這些執行緒,執行緒執行結束後則返回值將會被依次儲存在thread[x].result()中,此時直接將其相加即可得到最終執行緒執行結果;

#include <QCoreApplication>
#include <iostream>
#include <QThread>

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

    MyThread thread[3];

    // 分別將不同的引數傳入到執行緒函式內
    for(int x=0; x<3; x++)
    {
        thread[x].set_value(1,2);
        thread[x].setObjectName(QString("thread -> %1").arg(x));
        thread[x].start();
    }

    // 等待所有執行緒執行結束
    for(int x=0; x<3; x++)
    {
        thread[x].get_object_name();
        thread[x].wait();
    }

    // 獲取執行緒返回值並相加
    int result = thread[0].result() + thread[1].result() + thread[2].result();
    std::cout << "sum => " << result << std::endl;

    return a.exec();
}

程式執行後,則可以輸出三個執行緒相加的和;

1.3 互斥同步執行緒鎖

QMutex 是Qt框架中提供的用於執行緒同步的類,用於實現互斥訪問共享資源。Mutex是“互斥鎖(Mutual Exclusion)”的縮寫,它能夠確保在任意時刻,只有一個執行緒可以訪問被保護的資源,從而避免了多執行緒環境下的資料競爭和不一致性。

在Qt中,QMutex提供了簡單而有效的執行緒同步機制,其基本用法包括:

  • 鎖定(Lock): 執行緒在訪問共享資源之前,首先需要獲取QMutex的鎖,這透過呼叫lock()方法來實現。
  • 解鎖(Unlock): 當執行緒使用完共享資源後,需要釋放QMutex的鎖,以允許其他執行緒訪問,這透過呼叫unlock()方法來實現。

該鎖lock()鎖定與unlock()解鎖必須配對使用,執行緒鎖保證執行緒間的互斥,利用執行緒鎖能夠保證臨界資源的安全性。

  • 執行緒鎖解決的問題: 多個執行緒同時操作同一個全域性變數,為了防止資源的無序覆蓋現象,從而需要增加鎖,來實現多執行緒搶佔資源時可以有序執行。
  • 臨界資源(Critical Resource): 每次只允許一個執行緒進行訪問 (讀/寫)的資源。
  • 執行緒間的互斥(競爭): 多個執行緒在同一時刻都需要訪問臨界資源。
  • 一般性原則: 每一個臨界資源都需要一個執行緒鎖進行保護。

我們以生產者消費者模型為例來演示鎖的使用方法,生產者消費者模型是一種併發程式設計中常見的同步機制,用於解決多執行緒環境下的協作問題。該模型基於兩類角色:生產者(Producer)和消費者(Consumer),它們透過共享的緩衝區進行協作。

主要特點和工作原理如下:

  1. 生產者:
    • 生產者負責產生一些資源或資料,並將其放入共享的緩衝區中。生產者在生產資源後,需要通知消費者,以便它們可以取走資源。
  2. 消費者:
    • 消費者從共享的緩衝區中取走資源,並進行相應的處理。如果緩衝區為空,消費者需要等待,直到有新的資源可用。
  3. 共享緩衝區:
    • 作為生產者和消費者之間的交換介質,共享緩衝區儲存被生產者產生的資源。它需要提供對資源的安全訪問,以防止競態條件和資料不一致性。
  4. 同步機制:
    • 生產者和消費者之間需要一些同步機制,以確保在正確的時機進行資源的生產和消費。典型的同步機制包括訊號量、互斥鎖、條件變數等。

生產者消費者模型的典型應用場景包括非同步任務處理、事件驅動系統、資料快取等。這種模型的實現可以透過多執行緒程式設計或使用訊息佇列等方式來完成。

首先在全域性中引入#include <QMutex>庫,並在全域性定義static QMutex執行緒鎖變數,接著我們分別定義兩個自定義執行緒函式,其中Producer代表生產者,而Customer則是消費者,生產者中負責每次產出一個隨機數並將其追加到g_store全域性變數內儲存,消費者則透過g_store.remove每次取出一個元素。

static QMutex g_mutex;      // 執行緒鎖
static QString g_store;     // 定義全域性變數

class Producer : public QThread
{
protected:
    void run()
    {
        int count = 0;

        while(true)
        {
            // 加鎖
            g_mutex.lock();

            g_store.append(QString::number((count++) % 10));
            std::cout << "Producer -> "<< g_store.toStdString() << std::endl;

            // 釋放鎖
            g_mutex.unlock();
            msleep(900);
        }
    }
};

class Customer : public QThread
{
protected:
    void run()
    {
        while( true )
        {
            g_mutex.lock();
            if( g_store != "" )
            {
                g_store.remove(0, 1);
                std::cout << "Curstomer -> "<< g_store.toStdString() << std::endl;
            }

            g_mutex.unlock();
            msleep(1000);
        }
    }
};

在主函式中分別定義兩個執行緒類,並依次執行它們;

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

    Producer p;
    Customer c;

    p.setObjectName("producer");
    c.setObjectName("curstomer");

    p.start();
    c.start();

    return a.exec();
}

至此,生產者產生資料,消費者消費資料;如下圖所示;

QMutexLocker 是Qt框架中提供的一個輔助類,它是在QMutex基礎上簡化版的執行緒鎖,QMutexLocker會保護加鎖區域,並自動實現互斥量的鎖定和解鎖操作,可以將其理解為是智慧版的QMutex鎖,透過 QMutexLocker可以確保在作用域內始終持有鎖,從而避免因為忘記釋放鎖而導致的問題。該鎖只需要在上方程式碼中稍加修改即可。

使用 QMutexLocker 的一般流程如下:

  1. 建立一個 QMutex 物件。
  2. 建立一個 QMutexLocker 物件,傳入需要鎖定的 QMutex
  3. QMutexLocker 物件的作用域內進行需要互斥訪問的操作。
  4. QMutexLocker 物件超出作用域範圍時,會自動釋放鎖。
static QMutex g_mutex;      // 執行緒鎖
static QString g_store;     // 定義全域性變數

class Producer : public QThread
{
protected:
    void run()
    {
        int count = 0;

        while(true)
        {
			// 增加智慧執行緒鎖
            QMutexLocker Locker(&g_mutex);

            g_store.append(QString::number((count++) % 10));
            std::cout << "Producer -> "<< g_store.toStdString() << std::endl;

            msleep(900);
        }
    }
};

1.4 讀寫同步執行緒鎖

QReadWriteLock 是Qt框架中提供的用於實現讀寫鎖的類。讀寫鎖允許多個執行緒同時讀取共享資料,但在寫入資料時會互斥,確保資料的一致性和完整性。這對於大多數情況下讀取頻繁而寫入較少的共享資料非常有用,可以提高程式的效能。

其提供了兩種鎖定操作:

  • 讀取鎖(Read Lock): 允許多個執行緒同時獲取讀取鎖,用於並行讀取共享資料。在沒有寫入鎖的情況下,多個執行緒可以同時持有讀取鎖。
  • 寫入鎖(Write Lock): 寫入鎖是互斥的,當一個執行緒獲取寫入鎖時,其他執行緒無法獲取讀取鎖或寫入鎖。這確保了在寫入資料時,不會有其他執行緒同時讀取或寫入。

互斥鎖存在一個問題,每次只能有一個執行緒獲得互斥量的許可權,如果在程式中有多個執行緒來同時讀取某個變數,那麼使用互斥量必須排隊,效率上會大打折扣,基於QReadWriteLock讀寫模式進行程式碼段鎖定,即可解決互斥鎖存在的問題。

#include <QCoreApplication>
#include <iostream>
#include <QThread>
#include <QMutex>
#include <QReadWriteLock>

static QReadWriteLock g_mutex;      // 執行緒鎖
static QString g_store;             // 定義全域性變數

class Producer : public QThread
{
protected:
    void run()
    {
        int count = 0;

        while(true)
        {
            // 以寫入方式鎖定資源
            g_mutex.lockForWrite();

            g_store.append(QString::number((count++) % 10));

            // 寫入後解鎖資源
            g_mutex.unlock();

            msleep(900);
        }
    }
};

class Customer : public QThread
{
protected:
    void run()
    {
        while( true )
        {
            // 以讀取方式寫入資源
            g_mutex.lockForRead();
            if( g_store != "" )
            {
                std::cout << "Curstomer -> "<< g_store.toStdString() << std::endl;
            }

            // 讀取到後解鎖資源
            g_mutex.unlock();
            msleep(1000);
        }
    }
};

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

    Producer p1,p2;
    Customer c1,c2;

    p1.setObjectName("producer 1");
    p2.setObjectName("producer 2");

    c1.setObjectName("curstomer 1");
    c2.setObjectName("curstomer 2");

    p1.start();
    p2.start();

    c1.start();
    c2.start();

    return a.exec();
}

該鎖允許使用者以同步讀lockForRead()或同步寫lockForWrite()兩種方式實現保護資源,但只要有一個執行緒在以寫的方式操作資源,其他執行緒也會等待寫入操作結束後才可繼續讀資源。

1.5 基於訊號執行緒鎖

QSemaphore 是Qt框架中提供的用於實現訊號量的類。訊號量是一種用於線上程之間進行同步和通訊的機制,它允許多個執行緒在某個共享資源上進行協調,控制對該資源的訪問。QSemaphore 的主要作用是維護一個計數器,執行緒可以透過獲取和釋放訊號量來改變計數器的值。

其主要方法包括:

  • QSemaphore(int n = 0):建構函式,建立一個初始計數值為 n 的訊號量。
  • void acquire(int n = 1):獲取訊號量,將計數器減去 n。如果計數器不足,執行緒將阻塞等待。
  • bool tryAcquire(int n = 1):嘗試獲取訊號量,如果計數器足夠,立即獲取並返回 true;否則返回 false
  • void release(int n = 1):釋放訊號量,將計數器加上 n。如果有等待的執行緒,其中一個將被喚醒。

訊號量是特殊的執行緒鎖,訊號量允許N個執行緒同時訪問臨界資源,透過acquire()獲取到指定資源,release()釋放指定資源。

#include <QCoreApplication>
#include <iostream>
#include <QThread>
#include <QSemaphore>

const int SIZE = 5;
unsigned char g_buff[SIZE] = {0};

QSemaphore g_sem_free(SIZE); // 5個可生產資源
QSemaphore g_sem_used(0);    // 0個可消費資源

// 生產者生產產品
class Producer : public QThread
{
protected:
    void run()
    {
        while( true )
        {
            int value = qrand() % 256;

            // 若無法獲得可生產資源,阻塞在這裡
            g_sem_free.acquire();

            for(int i=0; i<SIZE; i++)
            {
                if( !g_buff[i] )
                {
                    g_buff[i] = value;
                    std::cout << objectName().toStdString() << " --> " << value << std::endl;
                    break;
                }
            }

            // 可消費資源數+1
            g_sem_used.release();

            sleep(2);
        }
    }
};

// 消費者消費產品
class Customer : public QThread
{
protected:
    void run()
    {
        while( true )
        {
            // 若無法獲得可消費資源,阻塞在這裡
            g_sem_used.acquire();

            for(int i=0; i<SIZE; i++)
            {
                if( g_buff[i] )
                {
                    int value = g_buff[i];

                    g_buff[i] = 0;
                    std::cout << objectName().toStdString() << " --> " << value << std::endl;
                    break;
                }
            }

            // 可生產資源數+1
            g_sem_free.release();

            sleep(1);
        }
    }
};

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

    Producer p1;
    Customer c1;

    p1.setObjectName("producer");
    c1.setObjectName("curstomer");

    p1.start();
    c1.start();

    return a.exec();
}

相關文章