线程与信号槽
- 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();// 这里可以处理工作线程完成后的逻辑
}