文章目錄
- 前言
- 1. Qt 多線程概述
- 2. QThread 常用 API
- 3. 使用線程
- 4. 多線的使用場景
- 5. 線程安全問題
- 5.1. 加鎖
- 5.2. QReadWriteLocker、QReadLocker、QWriteLocker
- 6. 條件變量 與 信號量
- 6.1. 條件變量
- 6.2 信號量
- 總結
前言
在現代軟件開發中,多線程編程已成為一個不可或缺的技能,尤其是在需要處理復雜任務和提高應用程序性能的場合。Qt,作為一個跨平臺的應用程序框架,提供了強大的多線程支持,使得開發者能夠充分利用多核處理器的優勢,開發出響應迅速且高效的應用程序。本文將深入探討Qt多線程的基本概念、API使用、線程安全問題以及同步機制,旨在幫助開發者更好地理解和運用Qt的多線程功能。
1. Qt 多線程概述
Qt 多線程 和 Linux 中線程,本質是一個東西。
Linux 中的各種和線程相關的 原理 和 注意事項,都是在Qt中適用的。
Qt 中的多線程 API
Linux 中的多線程 API,Linux 系統提供的 pthread 庫,Qt 中針對系統提供的線程 API 重新封裝了。
C++ 11 中,也引入了線程 std::thread
Linux 原生多線程 API,設計的非常差,使用起來非常不方便(也是 C 語言本身的局限性引起的)實際開發中,很少使用原生 api
std::thread
要比 Linux 的 API 要更好一些
Qt 中的多線程 API,還要好一點,其實參考了 Java 中的線程庫 API 的設計方式。
QThread
要想創建線程,就要創建出這樣的實例,創建線程的時候,需要重點指定線程的入口函數。創建一個 QThread
的子類,重寫其中 run
函數,起到指定函數入口的方式(多態)
(C++ 中這種做法,不算特別常見,相比之下 std::thread
直接指定回調的方式更常見一些,有些 C++ 的大佬,認為多態機制,帶來運行時的額外開銷(運行時,查詢虛函數表,找到對應的函數再執行))
有些場景確實對于性能追求到極致(游戲引擎,AI,做高性能服務器…)
Qt 做客戶端開發,客戶端性能只要不太拉跨就行!
性能從來不是Qt優先追求的
2. QThread 常用 API
start()
: 這個操作就是真正調用系統 API 創建線程,新的線程創建出來之后自然就會自動執行 run
函數。
可以使用 wait
, 讓一個線程等待另一個線程執行結束
3. 使用線程
實例:
之前基于定時器,寫過倒計時這樣的程序。
也可以通過線程,來完成類似的功能。定時器內部本質上也是可以基于多線程來實現的。(Qt 的定時器是否基于多線程,不太清楚)
創建另一個線程,新線程中,進行計時(搞一個循環,每循環一次,sleep 1s,sleep完成,就可以更新界面了)
由于存在線程安全問題,多個線程時對于界面的狀態進行修改,此時就會導致界面就出錯了。Qt選擇了一刀切!針對界面控件狀態進行任何修改,務必在主線程中執行。
// widget.h
#ifndef WIDGET_H
#define WIDGET_H#include <QWidget>
#include "thread.h"QT_BEGIN_NAMESPACE
namespace Ui { class Widget; }
QT_END_NAMESPACEclass Widget : public QWidget
{Q_OBJECTpublic:Widget(QWidget *parent = nullptr);~Widget();Thread thread;void handle();
private:Ui::Widget *ui;
};
#endif // WIDGET_H
// widget.cpp
#include "widget.h"
#include "ui_widget.h"Widget::Widget(QWidget *parent): QWidget(parent), ui(new Ui::Widget)
{ui->setupUi(this);// 連接信號槽,通過槽函數跟新界面connect(&thread, &Thread::notify, this, &Widget::handle);// 要啟動一下線程thread.start();
}Widget::~Widget()
{delete ui;
}void Widget::handle()
{// 此處修改界面內容int value = ui->lcdNumber->intValue();value--;ui->lcdNumber->display(value);
}
// thread.h
#ifndef THREAD_H
#define THREAD_H#include <QWidget>
#include <QThread>class Thread : public QThread
{Q_OBJECT
public:Thread();// 要用的目的是重寫父類的方法 run 方法void run();signals:void notify(); // 只用聲明不用定義
};#endif // THREAD_H
// thread.cpp
#include "thread.h"Thread::Thread()
{}void Thread::run()
{// 在這個 run 中。能否直接去進行修改界面內容呢?// 不可以!!!// 雖然不可以修改界面,但是可以針對時間來進行計時// 當每到一秒鐘的時候,通過信號槽,來通知主線程,負責更新界面內容for (int i = 0; i < 10; ++i) {// sleep 本身是 QThead 的成員函數, 就可以直接調用sleep(1);// 發送一個信號,通知主線程emit notify();}
}
4. 多線的使用場景
之前學習多線程,主要還是站在服務器開發的角度來看待的。
當時談到多線程,最主要的目的,是為了充分利用多核 CPU 的計算資源。雙路 CPU(一個主板上有兩個CPU)。
客戶端,多線程仍然非常有意義,側重點就不同了,對于普通用戶來說,“使用體驗”是一個非常重要的話題。
如果“非常快”的代價是“系統很卡”用戶大概率是不會買賬的,雖然普通用戶的家用 PC 上也是多核CPU,客戶端上的程序很少會使用多線程把 CPU 計算資源吃完。
相比之下,客戶端的多線程,主要是用于,通過多線程的方式,執行一些耗時的操作,避免主線程被卡死,避免對用戶造成一些不好的體驗。
比方說,客戶端經常會和服務器進行網絡通信,比方說客戶端要上傳/下載一個很大的文件,傳輸需要好久(20分鐘)(像這樣就是算是密集的IO操作,比如代碼中持續不斷的進行 QFile.write)這種密集 IO 就會使程序被系統阻塞,掛起;一旦進程都被掛起了,此時意味著,用戶進行各種操作,程序都無響應。(比如,啟動吃雞,啟動過程中就需要從文件/網絡 加載大量的資源,此時如果你狂點鼠標窗口,很可能這個窗口就僵住了)“WIndows 提示你這個窗口不能響應,是否要強制結束!”
因此,相比之下,更好的做法,使用單獨的線程,來處理這種密集 IO 操作,要掛起也是掛起這個新的線程。主線程負責用戶的各種操作,此時主線程仍然可以繼續工作,繼續響應用戶的各種操作。
5. 線程安全問題
多線程程序太復雜了
5.1. 加鎖
把多個線程訪問的公共資源,通過鎖保護起來。把并發執行變成串行執行。
Linux: mutex
互斥量。
C++11: 引入 std::mutex
Qt 同樣也提供了對應的鎖,來針對系統提供的鎖進行封裝。
QMutex
: lock 加鎖, unlock 解鎖。
void Thread::run()
{for (int i = 0; i < 50000; ++i) {++num;}
}
#include "mainwindow.h"
#include "ui_mainwindow.h"#include "thread.h"
#include <QDebug>MainWindow::MainWindow(QWidget *parent): QMainWindow(parent), ui(new Ui::MainWindow)
{ui->setupUi(this);// 創建兩個線程對象Thread t1;Thread t2;t1.start();t2.start();// 加上線程的等待,讓主線程等待這兩線程執行結束t1.wait();t2.wait();// 打印結果qDebug() << Thread::num;
}MainWindow::~MainWindow()
{delete ui;
}
由于三個線程之間是并發執行的關系,當 t1 和 t2 運行起來之后,主線程仍然會繼續往后執行,執行到打印的時候,大概率 t1、t2 還沒執行呢,所以要加上wait
,進行阻塞等待!
最后打印出來的結果并不是我們預期中的100,000 !說明是存在bug,說明是存在線程安全問題!
// 創建鎖對象
static QMutex mutex;
多個線程加鎖的對象,得是同一個對象!不同對象,此時不會產生鎖的互斥,也就無法把 并發執行 -> 串行執行,也就無法解決上述問題。
#include "thread.h"int Thread::num = 0;
QMutex Thread::mutex;Thread::Thread()
{}void Thread::run()
{for (int i = 0; i < 50000; ++i) {mutex.lock();++num;mutex.unlock();}
}
++num;
是一個兩個線程訪問的公共變量,之前如果并發執行,就可能第一個線程修改了一半,第二個線程也進行了修改,就容易出現問題。(++操作對應 三個cpu指令,在操作系統中詳細介紹)
加了鎖之后,第一個線程順利拿到鎖,繼續執行++
,在第一個線程沒有執行完的時候,第二個線程也嘗試枷鎖,就會阻塞等待。一直等待到第一個線程加鎖,第二個線程才可能從阻塞中被喚醒。
for (int i = 0; i < 50000; ++i) {mutex.lock();++num;mutex.unlock();
}
像這里的鎖,很容易忘記unlock
,實際開發中, 加鎖之后,涉及到的邏輯可能很復雜,下面很容易就忘記釋放鎖。
比如下面,也算是沒釋放鎖:
mutex.lock();
if (...) {return;
}
mutex.unlock();
或者,拋出異常,釋放動態內存,也會存在類似的問題。
C++ 引入 智能指針,就是為了解決上述的問題。
C++11 引入了 std::lock_guard
, 相當于是 std::lock_guard
, 相當于是 std::mutex
智能指針, 借助 RAII 機制。
{std::lock_guard guard(mutex);// 執行各種邏輯...
} // 大括號執行完畢,guard 變量的聲明周期結束,在析構的時候,執行unlock了。
上述方案,Qt 也參考過來了: QMutexLocker
#include "thread.h"
#include <QMutexLocker>int Thread::num = 0;
QMutex Thread::mutex;Thread::Thread()
{}void Thread::run()
{for (int i = 0; i < 50000; ++i) {QMutexLocker locker(&mutex);// mutex.lock();++num;// mutex.unlock();}
}
Qt 的鎖 和 C++標準庫中的鎖,本質上都是封裝的系統提供的鎖,編寫多線程代碼的時候,可以使用 Qt 的鎖,也可以使用 C++ 的鎖。
C++ 的鎖能鎖Qt 的線程嗎? 是可以的!(雖然混著用也行,但一般不建議)
5.2. QReadWriteLocker、QReadLocker、QWriteLocker
特點:
QReadWriteLock
是讀寫鎖類,用于控制讀和寫的并發訪問。
QReadLocker
用于讀操作上鎖,允許多個線程同時讀取共享資源。
QWriteLocker
用于寫操作上鎖,只允許一個線程寫入共享資源。
用途:在某些情況下,多個線程可以同時讀取共享數據,但只有一個線程能夠進行寫操作。讀寫鎖提供了更高效的并發訪問方式。
QReadWriteLock rwLock;
//在讀操作中使?讀鎖
{QReadLocker locker(&rwLock); //在作?域內?動上讀鎖//讀取共享資源//...} //在作?域結束時?動解讀鎖
//在寫操作中使?寫鎖
{QWriteLocker locker(&rwLock); //在作?域內?動上寫鎖//修改共享資源//...} //在作?域結束時?動解寫鎖
6. 條件變量 與 信號量
Qt 中的條件變量 與 信號量 和 Linux 中的條件變量、信號量一致。
6.1. 條件變量
多個線程,之間調度是無序的,為了能夠一定程度干預線程之間的順序引入條件變量。
在 Qt 中,專門提供了 QWaitCondition
類 來解決像上述這樣的問題。
wait
:中就會先釋放鎖 + 等待
要想釋放鎖,前提就是先獲取到鎖。
QMutex mutex;
QWaitCondition condition;
//在等待線程中
mutex.lock();
//檢查條件是否滿?,若不滿?則等待
while (!conditionFullfilled()) //
{condition.wait(&mutex); //等待條件滿?并釋放鎖
}
//條件滿?后繼續執?
//...
mutex.unlock();
//在改變條件的線程中
mutex.lock();
//改變條件
changeCondition();
condition.wakeAll(); //喚醒等待的線程
mutex.unlock();
判定線程繼續執行的條件是否成立,不成立就進行wait
等待。
這里要使用 while
判定而不是 if
,因為喚醒之后還需要確認一下當前條件是不是真的成立了。wait
可能被提前喚醒(可能被信號打斷了)
6.2 信號量
有時在多線程編程中,需要確保多個線程可以相應的訪問?個數量有限的相同資源。例如,運行程序的設備可能是非常有限的內存,因此我們更希望需要大量內存的線程將這?事實考慮在內,并根據可用的內存數量進行相關操作,多線程編程中類似問題通常用信號量來處理。信號量類似于增強的互斥鎖,不僅能完成上鎖和解鎖操作,而且可以跟蹤可用資源的數量。
特點:QSemaphore 是 Qt 框架提供的計數信號量類,用于控制同時訪問共享資源的線程數量。
用途:限制并發線程數量,用于解決?些資源有限的問題。
QSemaphore semaphore(2); //同時允許兩個線程訪問共享資源
//在需要訪問共享資源的線程中
semaphore.acquire(); //嘗試獲取信號量,若已滿則阻塞
//訪問共享資源
//...
semaphore.release(); //釋放信號量
//在另?個線程中進?類似操作
總結
本文詳細介紹了Qt多線程的各個方面,從基礎概念到實際應用,再到線程安全和同步機制的討論。首先,我們概述了Qt多線程與Linux線程的關系,并比較了Qt、C++11和Linux原生API的優缺點。接著,我們深入探討了QThread的常用API和如何使用線程來執行耗時操作,同時強調了Qt中界面更新必須在主線程中進行的原則。
在多線程的使用場景中,我們討論了多線程在客戶端開發中的重要性,尤其是在提升用戶體驗方面的作用。隨后,文章重點討論了線程安全問題,包括加鎖機制、讀寫鎖以及條件變量和信號量的使用,這些都是確保多線程程序正確運行的關鍵技術。
最后,通過實際代碼示例,我們展示了如何在Qt中創建和管理線程,以及如何使用鎖和其他同步機制來處理線程間的通信和數據共享。通過本文的學習,開發者應該能夠更加自信地在Qt中實現多線程編程,編寫出既高效又穩定的應用程序。