问题

问个关于QThread的问题,以下两种方式的执行效率有何区别?

回答
好的,我们来详细分析一下在 Qt 中使用 `QThread` 的两种常见方式及其执行效率的区别。

在 Qt 中,`QThread` 本身是一个用于管理线程的类,但它并不直接执行你的代码。通常我们需要将要执行的代码逻辑封装到一个继承自 `QObject` 的类中,然后将这个对象移动到 `QThread` 的实例中去执行。这里我们探讨的两种方式,主要区别在于 如何将任务对象关联到 `QThread` 并启动执行。

两种常见方式的概述

为了清晰地说明,我们假设你要执行一个耗时任务,比如一个循环计算或者网络请求。我们将其封装在一个 `Worker` 类中,该类继承自 `QObject`。

方式一:使用 `moveToThread()` 和 `QThread::start()`

```cpp
// Worker.h
include

class Worker : public QObject
{
Q_OBJECT
public:
explicit Worker(QObject parent = nullptr);

public slots:
void doWork();

signals:
void workFinished();
};

// Worker.cpp
include "Worker.h"
include
include

Worker::Worker(QObject parent) : QObject(parent) {}

void Worker::doWork() {
qDebug() << "Worker thread:" << QThread::currentThreadId();
for (int i = 0; i < 1000000; ++i) {
// 模拟耗时操作
volatile int temp = i i;
}
emit workFinished();
}

// main.cpp (示例)
include
include
include "Worker.h"

int main(int argc, char argv[]) {
QCoreApplication a(argc, argv);

QThread thread = new QThread;
Worker worker = new Worker;

// 1. 将 worker 对象移动到新线程
worker>moveToThread(thread);

// 2. 连接信号和槽
// 当线程启动时,执行 doWork 槽函数
QObject::connect(thread, &QThread::started, worker, &Worker::doWork);
// 当 doWork 完成时,发出 workFinished 信号
QObject::connect(worker, &Worker::workFinished, thread, &QThread::quit);
// 当 doWork 完成时,清理 worker 对象
QObject::connect(worker, &Worker::workFinished, worker, &QObject::deleteLater);
// 当 doWork 完成时,清理 thread 对象
QObject::connect(thread, &QThread::finished, thread, &QObject::deleteLater);

// 3. 启动线程
thread>start();

return a.exec();
}
```

详细解释方式一:

1. 创建 `QThread` 和 `Worker` 对象: 你分别创建了一个 `QThread` 对象和一个 `Worker` 对象(继承自 `QObject`)。
2. `worker>moveToThread(thread);`: 这是关键的一步。它将 `worker` 对象及其关联的信号和槽移动到由 `thread` 管理的新线程中。这意味着从现在开始,任何发送自 `worker` 的信号都会在 `thread` 的事件循环中处理,并且任何连接到 `worker` 的槽函数(如果它们在 `worker` 的上下文中被调用)也将在 `thread` 的事件循环中执行。
3. 连接信号和槽:
`QObject::connect(thread, &QThread::started, worker, &Worker::doWork);`:这是一个非常重要的连接。`QThread::started` 信号会在 `thread>start()` 被调用,并且线程成功创建并开始运行其事件循环之后发出。我们将其连接到 `worker` 的 `doWork` 槽函数,这样当新线程启动时,`doWork` 就会在该线程中执行。
`QObject::connect(worker, &Worker::workFinished, thread, &QThread::quit);`:当 `worker` 完成其任务并发出 `workFinished` 信号时,这个信号会触发 `QThread::quit()`。`QThread::quit()` 会使 `QThread` 的事件循环停止,并导致 `QThread::exec()` 返回,从而线程结束。
`QObject::connect(worker, &Worker::workFinished, worker, &QObject::deleteLater);` 和 `QObject::connect(thread, &QThread::finished, thread, &QObject::deleteLater);`:这些是用于清理资源的连接。当线程结束时,`worker` 和 `thread` 对象会被安全地删除。
4. `thread>start();`: 这会启动一个新的操作系统线程,并为其创建一个事件循环(如果它还没有的话)。
5. `a.exec();`: 主线程(GUI 线程)的事件循环继续运行,以处理用户界面和其他事件。

方式二:创建 `QThread` 的子类并重写 `run()`

```cpp
// MyThread.h
include
include

class MyThread : public QThread {
Q_OBJECT
public:
MyThread(QObject parent = nullptr);
protected:
void run() override;
signals:
void workFinished();
};

// MyThread.cpp
include "MyThread.h"
include

MyThread::MyThread(QObject parent) : QThread(parent) {}

void MyThread::run() {
qDebug() << "MyThread thread:" << QThread::currentThreadId();
for (int i = 0; i < 1000000; ++i) {
// 模拟耗时操作
volatile int temp = i i;
}
emit workFinished(); // 发出信号
}

// main.cpp (示例)
include
include "MyThread.h"

int main(int argc, char argv[]) {
QCoreApplication a(argc, argv);

MyThread myThread = new MyThread;

// 连接信号和槽
QObject::connect(myThread, &MyThread::workFinished, myThread, &QThread::quit);
// 当线程结束时,清理 myThread 对象
QObject::connect(myThread, &QThread::finished, myThread, &QObject::deleteLater);

// 启动线程
myThread>start();

return a.exec();
}
```

详细解释方式二:

1. 创建 `MyThread` 子类: 你创建了一个继承自 `QThread` 的类 `MyThread`。
2. 重写 `run()` 方法: 在 `MyThread` 的 `run()` 方法中,你直接编写了要执行的耗时逻辑。当线程启动时,`QThread::start()` 方法会调用 `run()` 方法。
3. `myThread>start();`: 这个调用会创建一个新的操作系统线程,并直接在其中执行重写后的 `run()` 方法。
4. 信号和槽:
你可以在 `run()` 方法中直接发出信号。这里我们发出了 `workFinished` 信号。
`QObject::connect(myThread, &MyThread::workFinished, myThread, &QThread::quit);`:这个连接的作用与方式一类似,当 `run()` 方法执行完毕并发出 `workFinished` 信号时,会调用 `QThread::quit()` 来停止线程的事件循环(如果存在的话)。
`QObject::connect(myThread, &QThread::finished, myThread, &QObject::deleteLater);`:当线程结束时,`myThread` 对象会被安全删除。

执行效率的区别分析

从 核心的线程启动和任务执行过程 来看,这两种方式的 基础效率是相当的。它们都是启动一个独立的操作系统线程来执行你的任务。

细微的效率和设计上的区别:

1. 代码组织和模型:
方式一 (`moveToThread`): 这是 Qt 推荐的 模型/视图(Model/View) 或者说 控制器(Controller)/模型(Model) 的线程管理方式。它将 任务逻辑 (Worker) 和 线程管理 (QThread) 分离。`Worker` 对象是被移动到线程中,它自身是一个 `QObject`,可以在自己的线程中接收信号并发出信号,这是更符合 Qt 的信号槽机制的通用模型。
优点:
灵活性高: `Worker` 对象可以被轻松地在不同的线程之间移动。
代码清晰: 职责分离,`Worker` 只管做它的工作,`QThread` 只管管理线程的生命周期。
易于测试: `Worker` 对象可以方便地在主线程或测试线程中实例化和调用其槽函数进行单元测试。
线程间通信更标准: 通过 `moveToThread`,`Worker` 对象就成为了目标线程的一部分,其信号和槽的发送和接收会遵循目标线程的事件循环,这使得线程间通信更加直观和安全。
缺点:
需要额外的 `connect(thread, &QThread::started, worker, &Worker::doWork);` 来启动任务。
需要 `worker>moveToThread(thread);` 这个显式的步骤。

方式二 (重写 `run`): 这种方式是将线程的 生命周期管理 和 任务执行 直接集成到同一个类中。`run()` 方法就是线程的入口点。
优点:
代码更紧凑: 将任务逻辑直接写在 `run()` 方法里,没有额外的 `worker` 对象,可能看起来更直接。
无需 `moveToThread`: 创建子类后直接 `start()` 即可。
缺点:
灵活性差: 这个 `MyThread` 对象本身就与这个特定的 `run()` 方法绑定,不容易将相同的任务逻辑迁移到另一个线程,或者在同一个线程中执行多个不同的任务。
信号槽集成稍显不便: 虽然 `MyThread` 继承自 `QThread`,本身也是 `QObject`,也可以发送信号。但如果你的任务逻辑比较复杂,需要多个函数或槽来协作,将其全部塞进 `run()` 方法会显得臃肿。通常,如果需要在重写 `run()` 的线程中接收信号并响应,还需要在 `MyThread` 类内部显式地创建一个 `QObject` 并在其构造函数中调用 `moveToThread`(这实际上又回到了方式一的思路)。

2. 事件循环的使用:
方式一: `QThread::start()` 启动线程并开始其事件循环 (`QThread::exec()`)。`moveToThread()` 将 `Worker` 放入这个事件循环中。当 `QThread::started` 信号发出时,`doWork()` 在线程的事件循环中执行。这是 Qt 推荐的方式,因为 Qt 的许多组件和异步操作都依赖于事件循环。
方式二: `QThread::start()` 会直接调用 `run()` 方法。`run()` 方法 默认情况下不会自动启动事件循环。如果你在 `run()` 方法中进行了阻塞操作,并且需要在这个线程中响应信号(比如其他线程发来的信号),那么你需要在 `run()` 方法的开头显式调用 `exec()`。如果 `run()` 方法执行完就结束了,那么这个线程也就结束了。

3. 内存管理和对象生命周期:
方式一: 使用 `moveToThread` 后,`worker` 对象的事件处理和槽函数的执行都在新线程的事件循环中。当线程结束时,通过 `deleteLater` 来安全地删除 `worker` 和 `thread` 对象,这是 Qt 中处理带有事件循环的对象的标准安全方式。
方式二: `MyThread` 对象本身就是线程的载体。当 `run()` 方法执行完毕,线程结束,通常通过 `finished` 信号触发 `deleteLater` 来删除 `MyThread` 对象。

总结效率对比:

任务执行本身: 两种方式的 CPU 消耗和计算速度几乎没有区别。它们都创建了一个新的操作系统线程,并且你的计算逻辑在其中运行。
线程创建和启动开销: 两种方式都需要创建操作系统线程,开销是相似的。
内存占用: 方式一多了一个 `Worker` 对象,但通常 `Worker` 对象很轻量级,这个额外的内存占用微乎其微,不足以影响整体效率。
主要区别在于设计模式和灵活性,而非底层执行速度。

什么情况下选择哪种方式?

推荐使用方式一 (`moveToThread`):
当你的任务逻辑比较复杂,可能需要多个函数或槽协同完成时。
当你希望将任务逻辑和线程管理清晰地分离,提高代码的可维护性和可重用性时。
当你需要在一个线程中处理大量的信号和槽,或者与其他 `QObject` 组件进行交互时。
当你需要更灵活地管理线程的生命周期,例如随时启动/停止,或者将同一类任务分发到不同的线程时。
当你考虑代码的长期可维护性和测试性时。

考虑方式二 (重写 `run`):
当你有一个非常简单、一次性的任务,并且不需要在线程中进行复杂的信号槽交互时。
你可能只是想简单地启动一个后台任务,并且任务执行完成后就结束。
例如,一个简单的文件读写,或者一个不涉及太多 Qt 控件交互的后台计算。

重要提示:

永远不要直接在 `QThread` 的子类中直接创建 `QObject` 然后在 `run()` 方法里 `exec()`,除非你明确知道你在做什么。 `QThread::start()` 会自动为新线程创建一个事件循环,并在线程启动后发出 `started` 信号。最常见的模式是 `moveToThread`,然后连接 `started` 信号来触发任务。
线程安全: 无论哪种方式,都要注意 共享数据的线程安全。避免在多个线程中直接修改同一个非线程安全对象。使用信号槽进行跨线程通信,或者使用互斥锁 (`QMutex`) 等机制来保护共享资源。

总而言之,方式一 (`moveToThread`) 是 Qt 中管理后台任务更通用、更推荐、更灵活的模式,它提供了更好的代码组织和可扩展性。虽然方式二看起来更直接,但在实际开发中,除非任务非常简单,否则方式一通常是更好的选择。在执行效率上,两者差异不大,主要在于开发和维护的成本。

网友意见

user avatar

你这种场景显然是 QThreadPool 最好,官方文档里连例子都写好了

       class HelloWorldTask : public QRunnable {     void run()     {         qDebug() << "Hello world from thread" << QThread::currentThread();     } }  HelloWorldTask *hello = new HelloWorldTask(); // QThreadPool takes ownership and deletes 'hello' automatically QThreadPool::globalInstance()->start(hello);      

要注意看Qt官方文档,QThreadPool 就在 QThread 的索引下一行很容易看到啊。

不要有问题就去百度瞎JB搜,去知乎问,都不是好方法,要先看官方文档。

类似的话题

本站所有内容均为互联网搜索引擎提供的公开搜索信息,本站不存储任何数据与内容,任何内容与数据均与本站无关,如有需要请联系相关搜索引擎包括但不限于百度google,bing,sogou

© 2025 tinynews.org All Rights Reserved. 百科问答小站 版权所有