Qt 是一個跨平臺C++圖形界面開發庫,利用Qt可以快速開發跨平臺窗體應用程式,在Qt中我們可以通過拖拽的方式將不同組件放到指定的位置,實現圖形化開發極大的方便了開發效率,本章將重點介紹如何運用`QThread`組件實現多線程功能。 ...
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),它們通過共用的緩衝區進行協作。
主要特點和工作原理如下:
- 生產者:
- 生產者負責產生一些資源或數據,並將其放入共用的緩衝區中。生產者在生產資源後,需要通知消費者,以便它們可以取走資源。
- 消費者:
- 消費者從共用的緩衝區中取走資源,併進行相應的處理。如果緩衝區為空,消費者需要等待,直到有新的資源可用。
- 共用緩衝區:
- 作為生產者和消費者之間的交換介質,共用緩衝區存儲被生產者產生的資源。它需要提供對資源的安全訪問,以防止競態條件和數據不一致性。
- 同步機制:
- 生產者和消費者之間需要一些同步機制,以確保在正確的時機進行資源的生產和消費。典型的同步機制包括信號量、互斥鎖、條件變數等。
生產者消費者模型的典型應用場景包括非同步任務處理、事件驅動系統、數據緩存等。這種模型的實現可以通過多線程編程或使用消息隊列等方式來完成。
首先在全局中引入#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
的一般流程如下:
- 創建一個
QMutex
對象。 - 創建一個
QMutexLocker
對象,傳入需要鎖定的QMutex
。 - 在
QMutexLocker
對象的作用域內進行需要互斥訪問的操作。 - 當
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();
}
文章出處:https://www.cnblogs.com/LyShark/p/18056212
本博客所有文章除特別聲明外,均採用 BY-NC-SA 許可協議。轉載請註明出處!