力学笃行(四)Qt 线程与信号槽

1. 主窗口(MainWindow)主线程

在Qt中,线程和信号槽机制是两个核心概念,它们结合使用可以实现多线程编程,并在不同线程之间进行通信。

这里提一个主线程的概念,主窗口(MainWindow)通常是应用程序的主要界面,它的生命周期和事件循环是由主线程管理的。虽然可以在主窗口的代码中创建和操作其他线程,但通常情况下,长时间运行的任务或耗时操作应该在单独的线程中执行,以保持主线程的响应性。

  1. 主线程的任务
    主线程负责处理用户界面交互、事件响应和更新UI等任务。长时间运行的任务应该在单独的线程中执行,以避免阻塞主线程并保持应用程序的响应性。

  2. 线程对象的生命周期
    在 mainwindow.cpp 中创建的线程对象 默认是属于主线程 的,因为它们是在主线程的上下文中创建的。即使在 mainwindow.cpp 中创建了一个 QThread 对象和其他工作线程对象,这些对象本身仍然属于主线程的管理

  3. 使用信号槽进行跨线程通信
    在 mainwindow.cpp 中创建的线程对象可以通过信号槽机制与其他对象或线程进行通信。这意味着你可以将主线程的信号连接到工作线程的槽,或者反过来,从工作线程发射信号并在主线程中处理。通过正确使用信号槽,可以实现跨线程的通信和数据传输,而不会阻塞主线程的事件循环。

2. 线程

2.1 QThread

Qt中使用QThread类来管理线程。一般来说,你可以通过以下步骤使用QThread:

  1. 创建一个线程类: 继承自QThread,重写run()方法,在run()方法中编写线程执行的代码。
  2. 启动线程: 通过创建线程对象并调用start()方法来启动线程。
  3. 线程的执行控制: 通常在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,可以使用信号和槽机制,或者在任务完成后通过主线程的事件循环执行相关操作。
  1. 基本语法
QFuture<void> QtConcurrent::run(Function function);
QFuture<void> QtConcurrent::run(Callable callable);

其中:

  • Function 是一个函数指针,指向要在后台线程中执行的函数。
  • Callable 是一个可调用对象,可以是函数对象或Lambda表达式等。
  1. 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中一种用于对象间通信的机制,它不仅可以在同一线程中使用,还可以跨线程使用。在跨线程的情况下,信号槽机制能够确保线程安全地进行通信。

  1. 定义信号和槽: 信号是类似于函数的成员,可以被其他对象连接到。槽是接收信号的函数,它们的声明方式与普通的C++成员函数相似,但使用signals和slots关键字来定义。

  2. 连接信号和槽: 使用connect()函数将信号与槽连接起来。Qt中支持跨线程的信号槽连接,当一个信号发射时,与之连接的槽可以在目标线程中被执行。

3.1 connect

在Qt中,使用connect()函数将信号与槽连接起来是实现对象间通信的核心机制之一。通过信号与槽的连接,可以在一个对象发出信号时,触发另一个对象的槽函数执行。下面是几种常见的连接方式示例:

  1. 普通连接方式
    最基本的连接方式是直接使用connect()函数将信号与槽连接起来。这种方式适用于信号和槽的参数列表完全匹配的情况。
// 连接 sender 对象的 signal 信号到 receiver 对象的槽函数 slot
connect(sender, SIGNAL(signal()), receiver, SLOT(slot()));

在这里:

  • sender 是发出信号的对象。
  • SIGNAL(signal()) 是宏,用于指定信号的名称。
  • receiver 是接收信号的对象。
  • SLOT(slot()) 是宏,用于指定槽函数的名称。
  1. 使用函数指针连接方式
    如果信号和槽的参数列表完全匹配,并且你希望避免使用宏,可以使用函数指针的方式连接。
// 连接 sender 对象的 signal 信号到 receiver 对象的槽函数 slot
connect(sender, &SenderClass::signal, receiver, &ReceiverClass::slot);

这种方式使用了C++11引入的新特性,使用函数指针取代了宏,更加类型安全。

  1. 使用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 表达式内部也能够修改当前对象的成员变量的值。
  1. 使用队列连接方式
    在Qt中,还可以使用Qt::QueuedConnection来连接信号和槽,这种方式将信号放入接收对象的事件队列中,在接收对象的事件循环中处理,即使信号和槽在不同的线程中也能正常工作。
// 使用队列连接方式,将 sender 对象的 signal 信号连接到 receiver 对象的槽函数 slot
connect(sender, SIGNAL(signal()), receiver, SLOT(slot()), Qt::QueuedConnection);

这种连接方式适用于需要在不同线程间进行通信的情况。

  1. 指定连接类型的应用

connect第五个参数

参数说明补充
Qt::AutoConnection如果信号和槽在同一线程,则使用Qt::DirectConnection;如果在不同线程,则使用Qt::QueuedConnection。默认值,使用这个值则连接类型会在信号发送时决定。如果接收者和发送者在同一个线程,则自动使用Qt::DirectConnection类型。如果接收者和发送者不在一个线程,则自动使用Qt::QueuedConnection类型。
Qt::DirectConnection直接调用槽函数,如果信号和槽在同一线程中,相当于直接调用函数。槽函数会在信号发送的时候直接被调用,槽函数运行于信号发送者所在线程。效果看上去就像是直接在信号发送位置调用了槽函数。这个在多线程环境下比较危险,可能会造成奔溃。
Qt::QueuedConnection将信号投递到接收者的事件队列中,在接收者的事件循环中处理,适合跨线程通信。槽函数在控制回到接收者所在线程的事件循环时被调用,槽函数运行于信号接收者所在线程。发送信号之后,槽函数不会立刻被调用,等到接收者的当前函数执行完,进入事件循不之后,槽函数才会被调用。多线程环境下一般用这个。
Qt::BlockingQueuedConnection特殊的队列连接方式,阻塞发送方直到槽函数执行完毕。槽函数的调用时机与Qt::QueuedConnection一致,不过发送完信号后发送者所在线程会阻塞,直到槽函数运行完。接收者和发送者绝对不能在一个线程,否则程序会死锁。在多线程间需要同步的场合可能需要这个。
Qt::UniqueConnectionQt::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_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 能够正确地处理参数的传递和槽函数的调用。

示例代码

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();
    // 这里可以处理工作线程完成后的逻辑
}
  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小老鼠不吃猫

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值