一、核心概念與 Qt 線程模型
1.線程與進程的區別:
線程是程序執行的最小單元,進程是資源分配的最小單元,線程共享進程的內存空間(堆,全局變量等),而進程擁有獨立的內存空間。Qt線程只要關注同一進程內的并發。
2.為什么使用多線程
當程序中有多個耗時的操作時候,為了提高性能,防止GUI線程阻塞,可以處理耗時操作
3.QThread 類的相關接口
start():啟動線程,調用run方法
run():線程的入口點,子類化 QThread 并重寫此方法是一種使用線程的方式(傳統方式)。線程在此函數中執行。
quit() / exit(int returnCode): 請求線程退出事件循環(如果正在運行)。
wait([unsigned long time = ULONG_MAX]): 阻塞調用線程,直到目標線程結束執行或超時。
isRunning() / isFinished(): 查詢線程狀態。
finished 信號:線程執行完畢(run() 返回)時發射。重要: 連接此信號進行資源清理(如 deleteLater)。
started 信號:線程啟動后(run() 執行前)發射。
4.事件循環 (Event Loop)
核心概念: 每個線程都可以擁有自己的事件循環(由 QEventLoop 管理)。主線程(GUI 線程)默認運行事件循環。
作用: 處理事件(如定時器事件、網絡事件、投遞的事件、隊列連接的信號槽調用)。
QThread::exec(): 進入事件循環(在 run() 方法中調用)。
Worker 對象 + moveToThread + 事件循環模式: 這是現代 Qt 多線程編程的主流模式。對象被移動到新線程后,它的槽函數將在新線程的事件循環中被調用。
6.信號與槽 (Signals & Slots) 的連接類型
Qt 的信號槽機制是線程安全的。
連接類型 (Connection Type) 決定槽函數在哪個線程執行:
Qt::AutoConnection (默認): 如果發送者和接收者在同一線程,行為同 DirectConnection;否則,行為同 QueuedConnection。
Qt::DirectConnection: 槽函數在發送者所在線程中立即執行(就像直接函數調用)。
Qt::QueuedConnection: 槽函數的調用被轉換為一個事件,放入接收者所在線程的事件隊列。接收者線程的事件循環稍后會從隊列中取出并執行該槽函數。這是跨線程通信最安全、最常用的方式!
Qt::BlockingQueuedConnection: 類似 QueuedConnection,但發送者線程會阻塞,直到接收者線程的槽函數執行完畢。慎用!容易死鎖。 確保接收者線程能及時處理事件。
Qt::UniqueConnection: 可以與上述類型組合使用 (AutoConnection | UniqueConnection),確保相同的信號和槽之間只有一個連接。
跨線程信號槽參數傳遞: 參數類型必須是 Qt 元對象系統已知的類型( 使用qRegisterMetaType() 注冊自定義類型)。對于 QueuedConnection 和 BlockingQueuedConnection,參數會被復制傳遞。
二、創建和管理線程
1.使用 QThread 的兩種主要模式:
使用 QThread 的兩種主要模式:
1.1 子類化 QThread (傳統方式):
(1)繼承 QThread。
(2)重寫 run() 方法,將需要在線程中執行的代碼放入其中。
(3)創建子類實例,調用 start()。
局限: run() 是唯一入口,難以處理多個任務或利用事件循環。對象本身(this)仍留在原線程(通常是主線程)。
在QThread子類的構造函數中創建的對象,其線程親和性是創建該QThread對象的線程(通常是主線程),而不是新線程。這會導致在該對象上使用定時器、信號槽等出現問題。正確的方式是在run()函數中創建這些對象,這樣它們的線程親和性才是新線程。但這樣又使得對象創建在run()函數內部,難以從外部管理。線程結束時要清理run()中創建的對象,需要自己管理,容易出錯。
1.2 Worker 對象 + moveToThread (推薦方式):
(1)創建一個普通的 QObject 子類(Worker 對象),它包含需要通過槽函數執行的任務。
(2)創建一個 QThread 實例。
(3)創建 Worker 對象實例(此時它在創建者線程,通常是主線程)。
(4)調用 workerObject->moveToThread(workerThread)。關鍵步驟!
(5)連接 Worker 對象的信號和槽(通常使用 QueuedConnection,但 AutoConnection 在 moveToThread 后通常也能正確處理)。
(6)啟動線程 workerThread->start()。這會啟動線程的事件循環。
(7)通過信號觸發 Worker 對象的槽函數執行任務。任務將在新線程中執行。
(8).請求線程退出:workerThread->quit() 或 workerThread->requestInterruption()(更安全)。
(9)連接 workerThread->finished() 信號到 workerThread->deleteLater() 和 workerObject->deleteLater() 進行自動清理。
//主線程中創建:QThread 實例和worker 對象
Worker* worker = new Worker();
QThread* thread = new QThread(this);
worker->moveToThread(thread );
//現在worker屬于新線程
thread->start(); // 內部調用exec()啟動事件循環
//通過信號槽提交任務
QObject::connect(this, &Controller::startTask, worker, &Worker::doWork);
// 線程結束時自動清理,不然手動在析構函數中delect worker也可以
connect(thread, &QThread::finished, worker, &QObject::deleteLater);
/*因為worker有指定父對象所有這兒不用刪除了
connect(thread, &QThread::finished, thread, &QObject::deleteLater); */// 在子線程執行耗時操作
void Worker::doWork() {............
}
關鍵原則:誰創建誰刪除,跨線程對象使用 deleteLater
新舊方式的對比
在 Worker 對象中不要做:
創建 QWidget 或其子類(GUI 對象必須在 GUI 線程創建)。
直接操作 GUI(通過信號通知 GUI 線程更新)。
優點: 更符合 Qt 對象模型,可以利用事件循環處理多個任務(定時器、網絡等),更靈活,資源管理更清晰。
2.線程池 (QThreadPool 和 QtConcurrent)
QThreadPool: 管理一組可重用的線程。用于執行 QRunnable 任務。
QRunnable: 定義需要執行的任務(重寫 run() 方法)。
QThreadPool::globalInstance(): 獲取全局線程池實例。
QThreadPool::start(QRunnable *task, int priority = 0): 提交任務到線程池。
優點: 避免頻繁創建銷毀線程的開銷,控制最大并發數。
QtConcurrent 命名空間: 提供高級 API 簡化并行計算,底層通常使用 QThreadPool。
run(Function function, …): 在單獨線程中運行函數。
map(), mapped(), filtered(), reduce() 等:對容器進行并行操作。
QFuture, QFutureWatcher: 用于監控異步計算的結果和狀態。
優點: 代碼簡潔,易于使用,適合數據并行任務。
三、線程同步與通信
1.互斥鎖 QMutex 保護共享數據訪問
2.讀寫鎖 (QReadWriteLock, QReadLocker, QWriteLocker) 優化“讀多寫少”場景。
3.信號量(QSemaphore) 控制對多個相同資源的訪問。
4.條件變量
允許線程在特定條件不滿足時睡眠等待,并在條件可能改變時被其他線程喚醒。
wait(QMutex *lockedMutex): 原子操作: 釋放 lockedMutex 并阻塞等待。被喚醒后,在返回前會重新獲取 lockedMutex。
wakeOne(): 喚醒一個等待的線程(任意)。
wakeAll(): 喚醒所有等待的線程。
經典模式: 生產者-消費者。
5.原子操作
對基本數據類型(整數、指針)提供無鎖的原子操作(讀、寫、加減、比較交換等)。
輕量級, 適用于簡單的計數器、標志位等場景。不能替代鎖保護復雜操作或多變量。
6.跨線程通信的首選:信號與槽
這是 Qt 中最安全、最便捷的跨線程通信機制。利用事件循環傳遞消息
四、線程安全與最佳實踐
1.GUI 線程規則 (黃金法則):
所有用戶界面操作(創建、訪問、更新 QWidget 及其子類)都必須在主線程(GUI 線程)中進行。
子線程需要更新 UI 時,必須通過信號槽(QueuedConnection)通知主線程進行更新。
2.資源管理
對象樹與所有權: Qt 的父子關系管理在跨線程時不適用。父對象和子對象必須在同一線程。
動態對象創建與銷毀:
在哪個線程創建對象,該對象通常就“屬于”那個線程。
使用 moveToThread() 改變所有權。
安全刪除: 使用 obj->deleteLater()。該方法會將刪除請求放入對象所在線程的事件隊列,由事件循環安全地執行刪除操作。這是跨線程刪除對象的正確方式! 特別是在連接 QThread::finished() 信號時使用。
3.避免死鎖
遵循鎖的固定順序。
最小化臨界區(持鎖時間)。
謹慎使用嵌套鎖。
優先使用 RAII 鎖管理 (QMutexLocker 等)。
避免在持鎖時等待另一個線程的信號(容易死鎖),或使用帶超時的等待。
4.退出
請求退出,而非強制終止 (terminate() 非常危險,可能導致資源泄露、狀態不一致,應避免使用)。
使用 QThread::requestInterruption() 設置中斷請求標志。
在 Worker 對象的耗時操作中定期檢查 QThread::isInterruptionRequested(),并在檢測到時提前退出。
調用 quit() 或 exit() 請求線程退出事件循環。
使用 wait()(可選,需設置合理超時)確保線程結束。
利用 finished() 信號進行清理 (deleteLater)。
5.異常處理
線程中的異常不會傳播到創建該線程的線程(如主線程)。
必須在 run() 或 Worker 對象的槽函數內部捕獲并處理所有可能的異常,否則會導致線程崩潰(整個進程通常不會退出,但該線程的工作停止)。
6.性能考量
線程創建銷毀有開銷,優先考慮線程池 (QThreadPool, QtConcurrent)。
同步原語(鎖)有開銷,盡量減少競爭(鎖粒度、讀寫鎖、原子操作)。
跨線程通信(信號槽 QueuedConnection)有事件投遞和復制的開銷。
平衡線程數量和任務粒度。