线程与信号槽
- [1. 主窗口(MainWindow)主线程](#1. 主窗口(MainWindow)主线程)
- [2. 线程](#2. 线程)
-
- [2.1 QThread](#2.1 QThread)
- [2.2 QtConcurrent::run()](#2.2 QtConcurrent::run())
- [2.3 thread 的调用方式](#2.3 thread 的调用方式)
- [3. 信号槽](#3. 信号槽)
-
- [3.1 connect](#3.1 connect)
- [3.2 元对象系统中注册自定义数据类型](#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类创建一个线程并启动它:
cpp
#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,可以使用信号和槽机制,或者在任务完成后通过主线程的事件循环执行相关操作。
- 基本语法
cpp
QFuture<void> QtConcurrent::run(Function function);
cpp
QFuture<void> QtConcurrent::run(Callable callable);
其中:
- Function 是一个函数指针,指向要在后台线程中执行的函数。
- Callable 是一个可调用对象,可以是函数对象或Lambda表达式等。
- Lambda表达式
cpp
QtConcurrent::run([&]() {
// 在后台线程中执行的代码
// 可以访问外部变量
});
Lambda表达式内部可以访问外部的变量,使用 [&] 捕捉方式可以捕捉所有外部变量的引用,使得在后台线程中可以安全地访问和修改这些变量。
以下是一个简单的示例,演示了如何使用 QtConcurrent::run() 执行一个耗时任务:
cpp
#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()函数将信号与槽连接起来。这种方式适用于信号和槽的参数列表完全匹配的情况。
cpp
// 连接 sender 对象的 signal 信号到 receiver 对象的槽函数 slot
connect(sender, SIGNAL(signal()), receiver, SLOT(slot()));
在这里:
- sender 是发出信号的对象。
- SIGNAL(signal()) 是宏,用于指定信号的名称。
- receiver 是接收信号的对象。
- SLOT(slot()) 是宏,用于指定槽函数的名称。
- 使用函数指针连接方式
如果信号和槽的参数列表完全匹配,并且你希望避免使用宏,可以使用函数指针的方式连接。
cpp
// 连接 sender 对象的 signal 信号到 receiver 对象的槽函数 slot
connect(sender, &SenderClass::signal, receiver, &ReceiverClass::slot);
这种方式使用了C++11引入的新特性,使用函数指针取代了宏,更加类型安全。
- 使用Lambda表达式连接方式
从Qt5开始,还可以使用Lambda表达式连接信号和槽。Lambda表达式可以捕获外部变量,使得连接的代码更加灵活和简洁。
三种常用使用方法
cpp
// 使用Lambda表达式连接 sender 对象的 signal 信号
connect(sender, &SenderClass::signal, [=](double* value) {
// Lambda表达式内的代码,可以执行任意操作
// 这里可以访问外部变量
receiver->slot();
});
cpp
connect(sender, &SenderClass::signal, [&](double* value) {
// Lambda表达式内的代码,可以执行任意操作
// 这里可以访问外部变量
receiver->slot();
});
cpp
connect(sender, &SenderClass::signal, [this](double* value) {
// Lambda表达式内的代码,可以执行任意操作
// 这里可以访问外部变量
receiver->slot();
});
Lambda表达式内部可以编写需要执行的逻辑,可以访问当前上下文中的变量。
捕获方式 | 捕获内容 | 权限 |
---|---|---|
[=] | 捕捉所有外部变量的副本 | 只能访问但不能修改 |
[&] | 捕捉所有外部变量的引用 | 可以修改这个信号参数的值 |
[this] | 捕捉当前对象的所有成员变量 | Lambda表达式内部可以访问当前对象的成员变量,但不能修改它们的值 |
第四种使用方法:访问和修改当前对象的成员变量
cpp
connect(sender, &SenderClass::signal, this, [this](double* value) {
// Lambda表达式内的代码,可以执行任意操作
// 这里可以访问外部变量
receiver->slot();
});
- 访问成员变量: 适合于连接信号时需要访问当前对象的成员变量的情况,例如在槽函数中需要使用类的状态或配置信息。
- 修改外部变量: 由于使用了 [this] 捕捉方式,Lambda 表达式内部也能够修改当前对象的成员变量的值。
- 使用队列连接方式
在Qt中,还可以使用Qt::QueuedConnection来连接信号和槽,这种方式将信号放入接收对象的事件队列中,在接收对象的事件循环中处理,即使信号和槽在不同的线程中也能正常工作。
cpp
// 使用队列连接方式,将 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()函数来连接信号与槽,并且注释了不同连接类型的使用场景:
cpp
#include <QObject>
class Sender : public QObject {
Q_OBJECT
public slots:
void sendSignal() {
emit someSignal();
}
signals:
void someSignal();
};
class Receiver : public QObject {
Q_OBJECT
public 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::DirectConnection
QObject::connect(&sender, SIGNAL(someSignal()), &receiver, SLOT(handleSignal()),
Qt::DirectConnection);
// 使用 Qt::QueuedConnection
QObject::connect(&sender, SIGNAL(someSignal()), &receiver, SLOT(handleSignal()),
Qt::QueuedConnection);
// 使用 Qt::BlockingQueuedConnection
QObject::connect(&sender, SIGNAL(someSignal()), &receiver, SLOT(handleSignal()),
Qt::BlockingQueuedConnection);
// 使用 Qt::UniqueConnection
bool 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 能够正确地处理参数的传递和槽函数的调用。
示例代码
cpp
namespace Test{
enum TestEnum {
TestA,
TestB,
TestC
};
}
cpp
qRegisterMetaType<Test::TestEnum>("Test::TestEnum");
附录一 信号槽机制与主线程进行通信示例
下面是一个简单的示例,展示了如何在 mainwindow.cpp 中创建一个工作线程,并通过信号槽机制与主线程进行通信。
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();
// 这里可以处理工作线程完成后的逻辑
}