在一开始写需求时,就决定在文件搜索时使用多线程了,不过当时想的是多线程搜索文件,而不是像这里一样:主线程(GUI 线程)主要负责处理用户界面和用户交互,搜索线程负责文件搜索逻辑。

多线程搜索文件:这个后面还是要实现的,先埋个坑
0722 更新:已经实现了,当时搜索到大量文件时会导致UI卡顿严重,初步猜测原因为线程之间交流频繁导致的

1. 为什么使用多线程

在开发 GUI 应用程序时,长时间运行的任务(如文件搜索)可能会导致界面冻结,影响用户体验。为了解决这个问题,我们可以使用多线程技术,将耗时的操作放到单独的线程中执行,从而保持用户界面的响应性。本文将详细介绍如何在 Qt 应用中实现多线程文件搜索。

2. 涉及的线程

在这个实现中,主要涉及两个线程:

  • 主线程(GUI 线程):负责处理用户界面和用户交互。
  • 文件搜索线程(工作线程):负责执行耗时的文件搜索操作。

3. 具体实现步骤

3.1 创建文件搜索线程类

我们创建了一个名为 FileSearchThread 的类,继承自 QThread,用于在单独的线程中执行文件搜索操作。

文件:FileSearchThread.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#ifndef FILESEARCHTHREAD_H
#define FILESEARCHTHREAD_H

#include <QThread>
#include <QString>
#include <QDirIterator>

class FileSearchThread : public QThread {
Q_OBJECT

public:
FileSearchThread(const QString &keyword, const QString &path, QObject *parent = nullptr);

void run() override;

signals:
void fileFound(const QString &filePath);
void searchFinished();

private:
QString searchKeyword;
QString searchPath;
};

#endif // FILESEARCHTHREAD_H

文件:FileSearchThread.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include "FileSearchThread.h"

FileSearchThread::FileSearchThread(const QString &keyword, const QString &path, QObject *parent)
: QThread(parent), searchKeyword(keyword), searchPath(path) {}

void FileSearchThread::run() {
QDirIterator it(searchPath, QDir::Files, QDirIterator::Subdirectories);
while (it.hasNext()) {
QString filePath = it.next();
if (filePath.contains(searchKeyword, Qt::CaseInsensitive)) {
emit fileFound(filePath);
}
}
emit searchFinished();
}
3.2 修改 FileSearch

FileSearch 类中,我们添加了信号和槽机制,以处理文件搜索线程的结果。

文件:FileSearch.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#ifndef FILESEARCH_H
#define FILESEARCH_H

#include <QWidget>
#include <QPushButton>
#include <QLineEdit>
#include <QListWidget>
#include "CustomModel.h"

namespace Ui {
class FileSearch;
}

class FileSearch : public QWidget {
Q_OBJECT

public:
explicit FileSearch(QWidget *parent = nullptr);
~FileSearch();

private slots:
void onSearchButtonClicked();
void onFileFound(const QString &filePath);
void onSearchFinished();

private:
Ui::FileSearch *ui;
CustomModel *resultModel;
QPushButton *searchButton;
QLineEdit *searchLineEdit;
QLineEdit *pathLineEdit;
QListWidget *resultListWidget;
};

#endif // FILESEARCH_H

文件:FileSearch.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
#include "FileSearch.h"
#include "ui_FileSearch.h"
#include "Logger.h"
#include "FileSearchThread.h"
#include <QDir>
#include <QDirIterator>
#include <QVBoxLayout>
#include <QDebug>
#include <QCoreApplication>

FileSearch::FileSearch(QWidget *parent) :
QWidget(parent),
ui(new Ui::FileSearch),
resultModel(new CustomModel(this)) // 使用自定义模型
{
ui->setupUi(this);

// 确保 UI 元素已经正确初始化
searchButton = ui->searchButton;
searchLineEdit = ui->searchLineEdit;
pathLineEdit = ui->pathLineEdit;
resultListWidget = ui->resultListWidget;

// 连接信号和槽
connect(searchButton, &QPushButton::clicked, this, &FileSearch::onSearchButtonClicked);

// 初始化 resultListWidget
qDebug() << "List view model set.";

// 检查是否已有布局,避免重复设置
if (!layout()) {
QVBoxLayout *layout = new QVBoxLayout(this);
layout->addWidget(resultListWidget);
setLayout(layout);
}
}

FileSearch::~FileSearch() {
delete ui;
}

void FileSearch::onSearchButtonClicked() {
QString searchKeyword = searchLineEdit->text();
QString searchPath = pathLineEdit->text();

// 如果路径为空,则设置为根目录
if (searchPath.isEmpty()) {
searchPath = QDir::rootPath();
}

// 清空之前的搜索结果
resultListWidget->clear();
Logger::instance().log("Search started for keyword: " + searchKeyword + " in path: " + searchPath);

// 创建并启动文件搜索线程
FileSearchThread *searchThread = new FileSearchThread(searchKeyword, searchPath, this);
connect(searchThread, &FileSearchThread::fileFound, this, &FileSearch::onFileFound);
connect(searchThread, &FileSearchThread::searchFinished, this, &FileSearch::onSearchFinished);
searchThread->start();
}

void FileSearch::onFileFound(const QString &filePath) {
resultListWidget->addItem(filePath);
Logger::instance().log("Found file: " + filePath);

// 每次找到文件后刷新视图
resultListWidget->reset();
resultListWidget->update();
resultListWidget->viewport()->update();
QCoreApplication::processEvents(); // 强制处理所有挂起的事件
}

void FileSearch::onSearchFinished() {
// 搜索完成后,滚动到第一个结果项
if (resultListWidget->count() > 0) {
resultListWidget->scrollToItem(resultListWidget->item(0));
}
}

// 添加以下行以包含生成的MOC文件
#include "filesearch.moc"

主要看一下这几行代码,

1
2
3
4
5
// 创建并启动文件搜索线程
FileSearchThread *searchThread = new FileSearchThread(searchKeyword, searchPath, this);
connect(searchThread, &FileSearchThread::fileFound, this, &FileSearch::onFileFound);
connect(searchThread, &FileSearchThread::searchFinished, this, &FileSearch::onSearchFinished);
searchThread->start();

创建和启动文件搜索线程

  • FileSearchThread 是一个自定义线程类,继承自 QThread,用于在单独的线程中执行文件搜索操作。构造函数传递了搜索关键字、路径和父对象。
  • connect 函数连接了 FileSearchThread 的信号和 FileSearch 的槽函数,以处理线程完成搜索时发出的信号。
  • 调用 start 方法启动线程,这会调用 FileSearchThreadrun 方法在新线程中执行文件搜索操作

4. 多线程相关技术要点

  1. QThread 的使用

QThread 是 Qt 提供的线程类,用于创建和管理线程。通过继承 QThread 并重写 run 方法,可以在子类中定义线程的工作内容。需要注意的是,run 方法中的代码会在新的线程中执行,而不是在主线程中。以下是一些使用 QThread 的注意事项:

  • 避免直接操作 GUI:在 run 方法中不要直接操作 GUI 元素,所有的 GUI 操作应通过信号和槽在主线程中完成。
  • 正确处理线程结束:在需要的地方调用 wait 方法,确保线程结束后再继续执行其他操作,避免资源泄露。
  • 线程优先级:可以通过 setPriority 方法设置线程的优先级,确保重要任务获得更多 CPU 时间。
  1. 信号和槽机制

Qt 的信号和槽机制是实现线程间通信的关键。通过在工作线程中发射信号,并在主线程中连接相应的槽函数,可以实现线程间的数据传递和同步。以下是信号和槽的定义和使用:

  • 信号定义:在 FileSearchThread 类中定义了 fileFoundsearchFinished 信号,用于通知主线程文件搜索的进度和完成状态。
  • 槽函数:在 FileSearch 类中定义了 onFileFoundonSearchFinished 槽函数,用于处理文件搜索线程发射的信号。
  • 自动连接机制:使用 connect 函数时,可以利用 Qt 的自动连接机制,基于命名约定自动连接信号和槽。
  1. 线程安全

在多线程环境中,确保线程安全是非常重要的。Qt 提供了一些机制来帮助实现线程安全:

  • QMutex:互斥锁,用于保护共享资源,防止多个线程同时访问导致数据不一致。
  • QWaitCondition:条件变量,用于线程间的同步,通常与 QMutex 一起使用。
  • QAtomic:提供了一些原子操作,用于实现无锁的线程安全操作,适用于简单的计数或标志位。

在本例中,由于文件搜索操作主要是读操作,没有涉及共享资源的写操作,因此不需要使用 QMutex。但在复杂的多线程应用中,使用 QMutexQWaitCondition 等机制来确保线程安全是必不可少的。

  1. 线程生命周期管理

在创建和使用线程时,必须注意线程的生命周期管理。以下是一些关键点:

  • 线程启动:通过调用 start 方法启动线程。
  • 线程结束:线程完成任务后会自动结束,可以通过连接 searchFinished 信号来处理线程结束后的操作。
  • 资源释放:确保在适当的时候释放线程对象,以避免内存泄漏。在本例中,FileSearchThread 对象是由 FileSearch 类管理的,因此在 FileSearch 类的析构函数中需要释放相关资源。

© 2024 Montee | Powered by Hexo | Theme stellar


Static Badge