線程與信號槽
- 1. 主窗口(MainWindow)主線程
- 2. 線程
- 2.1 QThread
- 2.2 QtConcurrent::run()
- 2.3 thread 的調用方式
- 3. 信號槽
- 3.1 connect
- 3.2 元對象系統中注冊自定義數據類型
- 附錄一 信號槽機制與主線程進行通信示例
1. 主窗口(MainWindow)主線程
在Qt中,線程和信號槽機制是兩個核心概念,它們結合使用可以實現多線程編程,并在不同線程之間進行通信。
這里提一個主線程的概念,主窗口(MainWindow)通常是應用程序的主要界面,它的生命周期和事件循環是由主線程管理的。雖然可以在主窗口的代碼中創建和操作其他線程,但通常情況下,長時間運行的任務或耗時操作應該在單獨的線程中執行,以保持主線程的響應性。
-
主線程的任務
主線程負責處理用戶界面交互、事件響應和更新UI等任務。長時間運行的任務應該在單獨的線程中執行,以避免阻塞主線程并保持應用程序的響應性。 -
線程對象的生命周期
在 mainwindow.cpp 中創建的線程對象 默認是屬于主線程 的,因為它們是在主線程的上下文中創建的。即使在 mainwindow.cpp 中創建了一個 QThread 對象和其他工作線程對象,這些對象本身仍然屬于主線程的管理。 -
使用信號槽進行跨線程通信
在 mainwindow.cpp 中創建的線程對象可以通過信號槽機制與其他對象或線程進行通信。這意味著你可以將主線程的信號連接到工作線程的槽,或者反過來,從工作線程發射信號并在主線程中處理。通過正確使用信號槽,可以實現跨線程的通信和數據傳輸,而不會阻塞主線程的事件循環。
2. 線程
2.1 QThread
Qt中使用QThread類來管理線程。一般來說,你可以通過以下步驟使用QThread:
- 創建一個線程類: 繼承自QThread,重寫run()方法,在run()方法中編寫線程執行的代碼。
- 啟動線程: 通過創建線程對象并調用start()方法來啟動線程。
- 線程的執行控制: 通常在run()方法中編寫線程的主要邏輯。可以通過信號槽機制在主線程和子線程之間進行通信。
下面是一個簡單的示例,演示如何使用QThread類創建一個線程并啟動它:
#include <QCoreApplication>
#include <QThread>
#include <QDebug>// 自定義的線程類
class WorkerThread : public QThread
{
public:void run() override{qDebug() << "Worker Thread ID: " << QThread::currentThreadId();// 執行一些耗時的任務for (int i = 0; i < 5; ++i) {qDebug() << "Counting " << i;sleep(1); // 模擬耗時操作}}
};int main(int argc, char *argv[])
{QCoreApplication a(argc, argv);qDebug() << "Main Thread ID: " << QThread::currentThreadId();WorkerThread thread;thread.start(); // 啟動線程// 這里可以繼續在主線程中執行其他任務return a.exec();
}
2.2 QtConcurrent::run()
在 Qt 中,QtConcurrent::run() 函數是用于在 后臺線程 中執行函數或Lambda表達式的便捷方法。它允許在不需要手動管理線程的情況下,并行地執行耗時的操作,從而避免主線程的阻塞和提高程序的響應性。
- 線程管理: 是一個線程安全的函數,它會在 Qt 的線程池中執行任務,避免了直接操作底層線程的復雜性。Qt 會自動管理線程池的大小和任務的分發,以提高效率和性能。
- 線程安全性: 由于任務在后臺線程中執行,必須確保訪問共享資源時的線程安全性,例如使用互斥量 (QMutex) 或原子操作來保護共享數據的訪問。
- UI 更新: 后臺線程中不能直接更新用戶界面 (UI),如需要在任務完成后更新 UI,可以使用信號和槽機制,或者在任務完成后通過主線程的事件循環執行相關操作。
- 基本語法
QFuture<void> QtConcurrent::run(Function function);
QFuture<void> QtConcurrent::run(Callable callable);
其中:
- Function 是一個函數指針,指向要在后臺線程中執行的函數。
- Callable 是一個可調用對象,可以是函數對象或Lambda表達式等。
- Lambda表達式
QtConcurrent::run([&]() {// 在后臺線程中執行的代碼// 可以訪問外部變量
});
Lambda表達式內部可以訪問外部的變量,使用 [&] 捕捉方式可以捕捉所有外部變量的引用,使得在后臺線程中可以安全地訪問和修改這些變量。
以下是一個簡單的示例,演示了如何使用 QtConcurrent::run() 執行一個耗時任務:
#include <QtConcurrent/QtConcurrent>// 定義一個耗時任務
void performTask(int value) {// 模擬耗時操作for (int i = 0; i < value; ++i) {QThread::msleep(100); // 模擬耗時操作,每次休眠100毫秒qDebug() << "Task progress:" << i;}
}int main(int argc, char *argv[])
{QCoreApplication a(argc, argv);int parameter = 5; // 任務的參數// 使用 QtConcurrent::run 啟動一個后臺任務QFuture<void> future = QtConcurrent::run([&]() {performTask(parameter);});// 等待任務完成future.waitForFinished();qDebug() << "Task completed!";return a.exec();
}
在這個示例中,performTask 函數模擬了一個耗時的任務,使用 QtConcurrent::run() 啟動一個后臺線程執行這個任務,并通過 QFuture 跟蹤任務的執行狀態和結果。
2.3 thread 的調用方式
參數 | 說明 |
---|---|
detach | 啟動的線程自主在后臺運行,當前的代碼繼續主下執行,不等待新線程結束。 |
join | 等待啟動的線程完成,才會繼續往下執行。 |
3. 信號槽
信號槽是Qt中一種用于對象間通信的機制,它不僅可以在同一線程中使用,還可以跨線程使用。在跨線程的情況下,信號槽機制能夠確保線程安全地進行通信。
-
定義信號和槽: 信號是類似于函數的成員,可以被其他對象連接到。槽是接收信號的函數,它們的聲明方式與普通的C++成員函數相似,但使用signals和slots關鍵字來定義。
-
連接信號和槽: 使用connect()函數將信號與槽連接起來。Qt中支持跨線程的信號槽連接,當一個信號發射時,與之連接的槽可以在目標線程中被執行。
3.1 connect
在Qt中,使用connect()函數將信號與槽連接起來是實現對象間通信的核心機制之一。通過信號與槽的連接,可以在一個對象發出信號時,觸發另一個對象的槽函數執行。下面是幾種常見的連接方式示例:
- 普通連接方式
最基本的連接方式是直接使用connect()函數將信號與槽連接起來。這種方式適用于信號和槽的參數列表完全匹配的情況。
// 連接 sender 對象的 signal 信號到 receiver 對象的槽函數 slot
connect(sender, SIGNAL(signal()), receiver, SLOT(slot()));
在這里:
- sender 是發出信號的對象。
- SIGNAL(signal()) 是宏,用于指定信號的名稱。
- receiver 是接收信號的對象。
- SLOT(slot()) 是宏,用于指定槽函數的名稱。
- 使用函數指針連接方式
如果信號和槽的參數列表完全匹配,并且你希望避免使用宏,可以使用函數指針的方式連接。
// 連接 sender 對象的 signal 信號到 receiver 對象的槽函數 slot
connect(sender, &SenderClass::signal, receiver, &ReceiverClass::slot);
這種方式使用了C++11引入的新特性,使用函數指針取代了宏,更加類型安全。
- 使用Lambda表達式連接方式
從Qt5開始,還可以使用Lambda表達式連接信號和槽。Lambda表達式可以捕獲外部變量,使得連接的代碼更加靈活和簡潔。
三種常用使用方法
// 使用Lambda表達式連接 sender 對象的 signal 信號
connect(sender, &SenderClass::signal, [=](double* value) {// Lambda表達式內的代碼,可以執行任意操作// 這里可以訪問外部變量receiver->slot();
});
connect(sender, &SenderClass::signal, [&](double* value) {// Lambda表達式內的代碼,可以執行任意操作// 這里可以訪問外部變量receiver->slot();
});
connect(sender, &SenderClass::signal, [this](double* value) {// Lambda表達式內的代碼,可以執行任意操作// 這里可以訪問外部變量receiver->slot();
});
Lambda表達式內部可以編寫需要執行的邏輯,可以訪問當前上下文中的變量。
捕獲方式 | 捕獲內容 | 權限 |
---|---|---|
[=] | 捕捉所有外部變量的副本 | 只能訪問但不能修改 |
[&] | 捕捉所有外部變量的引用 | 可以修改這個信號參數的值 |
[this] | 捕捉當前對象的所有成員變量 | Lambda表達式內部可以訪問當前對象的成員變量,但不能修改它們的值 |
第四種使用方法:訪問和修改當前對象的成員變量
connect(sender, &SenderClass::signal, this, [this](double* value) {// Lambda表達式內的代碼,可以執行任意操作// 這里可以訪問外部變量receiver->slot();
});
- 訪問成員變量: 適合于連接信號時需要訪問當前對象的成員變量的情況,例如在槽函數中需要使用類的狀態或配置信息。
- 修改外部變量: 由于使用了 [this] 捕捉方式,Lambda 表達式內部也能夠修改當前對象的成員變量的值。
- 使用隊列連接方式
在Qt中,還可以使用Qt::QueuedConnection來連接信號和槽,這種方式將信號放入接收對象的事件隊列中,在接收對象的事件循環中處理,即使信號和槽在不同的線程中也能正常工作。
// 使用隊列連接方式,將 sender 對象的 signal 信號連接到 receiver 對象的槽函數 slot
connect(sender, SIGNAL(signal()), receiver, SLOT(slot()), Qt::QueuedConnection);
這種連接方式適用于需要在不同線程間進行通信的情況。
- 指定連接類型的應用
connect第五個參數
參數 | 說明 | 補充 |
---|---|---|
Qt::AutoConnection | 如果信號和槽在同一線程,則使用Qt::DirectConnection;如果在不同線程,則使用Qt::QueuedConnection。 | 默認值,使用這個值則連接類型會在信號發送時決定。如果接收者和發送者在同一個線程,則自動使用Qt::DirectConnection類型。如果接收者和發送者不在一個線程,則自動使用Qt::QueuedConnection類型。 |
Qt::DirectConnection | 直接調用槽函數,如果信號和槽在同一線程中,相當于直接調用函數。 | 槽函數會在信號發送的時候直接被調用,槽函數運行于信號發送者所在線程。效果看上去就像是直接在信號發送位置調用了槽函數。這個在多線程環境下比較危險,可能會造成奔潰。 |
Qt::QueuedConnection | 將信號投遞到接收者的事件隊列中,在接收者的事件循環中處理,適合跨線程通信。 | 槽函數在控制回到接收者所在線程的事件循環時被調用,槽函數運行于信號接收者所在線程。發送信號之后,槽函數不會立刻被調用,等到接收者的當前函數執行完,進入事件循不之后,槽函數才會被調用。多線程環境下一般用這個。 |
Qt::BlockingQueuedConnection | 特殊的隊列連接方式,阻塞發送方直到槽函數執行完畢。 | 槽函數的調用時機與Qt::QueuedConnection一致,不過發送完信號后發送者所在線程會阻塞,直到槽函數運行完。接收者和發送者絕對不能在一個線程,否則程序會死鎖。在多線程間需要同步的場合可能需要這個。 |
Qt::UniqueConnection | Qt::UniqueConnection用于確保同一連接不會被重復建立。如果同一組件(sender 和 receiver)已經有一個相同類型的連接存在,則connect()函數會失敗并返回false。這種方式常用于確保只有一個唯一的連接存在,避免多次連接導致槽函數被多次調用。 | 這個flag可以通過按位或(1)與以上四個結合在一起使用。當這個flag設置時,當某個信號和槽已經連接愛時,再進行重復的連接就會失敗。也就是避免了重復連接。 |
斷開連接的方法 | 該方法雖然不是必須使用的,因為當一個對象delete之后,Qt自動取消所有連接到這個對象上面的槽。disconnect(sender,SIGNAL(signal),receiver,SLOT(slot), Qt::DirectConnection); |
下面是一個簡單的示例,演示了如何使用connect()函數來連接信號與槽,并且注釋了不同連接類型的使用場景:
#include <QObject>class Sender : public QObject {Q_OBJECTpublic slots:void sendSignal() {emit someSignal();}signals:void someSignal();
};class Receiver : public QObject {Q_OBJECTpublic slots:void handleSignal() {qDebug() << "Signal received in thread: " << QThread::currentThreadId();}
};int main(int argc, char *argv[]) {QCoreApplication app(argc, argv);Sender sender;Receiver receiver;// 使用 Qt::AutoConnection(默認)QObject::connect(&sender, SIGNAL(someSignal()), &receiver, SLOT(handleSignal()));// 使用 Qt::DirectConnectionQObject::connect(&sender, SIGNAL(someSignal()), &receiver, SLOT(handleSignal()),Qt::DirectConnection);// 使用 Qt::QueuedConnectionQObject::connect(&sender, SIGNAL(someSignal()), &receiver, SLOT(handleSignal()),Qt::QueuedConnection);// 使用 Qt::BlockingQueuedConnectionQObject::connect(&sender, SIGNAL(someSignal()), &receiver, SLOT(handleSignal()),Qt::BlockingQueuedConnection);// 使用 Qt::UniqueConnectionbool connected = QObject::connect(&sender, SIGNAL(someSignal()), &receiver, SLOT(handleSignal()),Qt::UniqueConnection);if (!connected) {qDebug() << "Failed to establish unique connection!";}// 發送信號sender.sendSignal();return app.exec();
}#include "main.moc"
3.2 元對象系統中注冊自定義數據類型
在Qt中,信號和槽(Signals and Slots)是一種強大的機制,用于在對象之間進行通信。Qt 會對于標準的數據類型(如 int、QString 等)進行內置支持,但對于自定義的數據類型(如枚舉、結構體、類等),Qt 需要能夠動態地識別和處理這些類型。因此,需要使用 qRegisterMetaType 來告知 Qt 系統如何處理這些自定義類型:
- 注冊類型: 通過 qRegisterMetaType,Qt 能夠在運行時了解如何創建、復制和銷毀這些類型的實例。
- 信號和槽的參數傳遞: 注冊后,可以在信號和槽的連接中使用這些自定義類型作為參數,Qt 能夠正確地處理參數的傳遞和槽函數的調用。
示例代碼
namespace Test{enum TestEnum {TestA,TestB,TestC};
}
qRegisterMetaType<Test::TestEnum>("Test::TestEnum");
附錄一 信號槽機制與主線程進行通信示例
下面是一個簡單的示例,展示了如何在 mainwindow.cpp 中創建一個工作線程,并通過信號槽機制與主線程進行通信。
// mainwindow.cpp#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QThread>
#include <QDebug>// 定義一個工作線程類
class WorkerThread : public QThread
{
public:void run() override{qDebug() << "Worker Thread ID: " << QThread::currentThreadId();// 模擬耗時操作for (int i = 0; i < 5; ++i) {qDebug() << "Counting " << i;sleep(1);}// 發射信號表示工作完成emit workFinished();}signals:void workFinished();
};MainWindow::MainWindow(QWidget *parent) :QMainWindow(parent),ui(new Ui::MainWindow)
{ui->setupUi(this);qDebug() << "Main Thread ID: " << QThread::currentThreadId();// 創建工作線程實例WorkerThread *workerThread = new WorkerThread();// 連接工作線程的工作完成信號到主線程的槽connect(workerThread, &WorkerThread::workFinished, this, &MainWindow::onWorkFinished);// 啟動工作線程workerThread->start();
}MainWindow::~MainWindow()
{delete ui;
}void MainWindow::onWorkFinished()
{qDebug() << "Work finished signal received in Main Thread ID: " << QThread::currentThreadId();// 這里可以處理工作線程完成后的邏輯
}