接上篇QT实现 端口扫描暂停和继续功能 3-CSDN博客
多线程与线程池的关系
- 多线程是基础: 线程池是基于多线程的概念实现的。线程池内部使用多个线程来并发执行任务。
- 线程池优化多线程: 线程池通过复用线程和管理任务来优化多线程的使用,减少了线程创建和销毁的开销,提高了系统的性能和响应速度。
- 任务调度: 在多线程环境中,开发者需要手动管理线程的创建、调度和销毁,而使用线程池可以简化这一过程,开发者只需提交任务,线程池会自动处理线程的分配和执行。
1. 修改 MyThread 类为 ScanTask 类
将 MyThread
类重命名为 ScanTask
,并使其继承自 QRunnable
。这样可以将每个端口扫描任务作为一个独立的任务提交到线程池中。
ScanTask.h
#ifndef SCANTASK_H
#define SCANTASK_H #include <QTcpSocket>
#include <QDebug>
#include <QRunnable>
#include <QObject> class ScanTask : public QObject, public QRunnable // 需要继承两个类
{ Q_OBJECT
public: ScanTask(QString strIP, int intPort); ~ScanTask(); void setPaused(bool paused); // 设置暂停状态 void closeThread(); // 关闭线程 protected: void run() override; // 重写 run 方法 private: volatile bool isStop; // 控制任务停止 volatile bool isPaused; // 控制任务暂停 QString m_strIP; // IP 地址 int m_intPort; // 端口号 signals: void send_scan_signal(int port, bool isOpen); // 发送扫描结果信号 void finished(int port, bool isOpen); // 扫描完成信号
}; #endif // SCANTASK_H
ScanTask.cpp
#include "scantask.h" ScanTask::ScanTask(QString strIP, int intPort) : m_strIP(strIP), m_intPort(intPort), isStop(false), isPaused(false) // 初始化成员变量
{
} ScanTask::~ScanTask()
{ qDebug("ScanTask deleted.");
} void ScanTask::setPaused(bool paused)
{ isPaused = paused; // 更新暂停状态
} void ScanTask::closeThread()
{ isStop = true; // 设置为停止状态
} void ScanTask::run()
{ QTcpSocket socket; socket.abort(); // 取消任何现有连接 // 执行端口扫描 if (isStop) return; // 检查是否停止 socket.connectToHost(m_strIP, m_intPort); // 连接到指定的 IP 和端口 if (socket.waitForConnected(1000)) // 等待连接 { emit send_scan_signal(m_intPort, true); // 发送信号,表示端口打开 qDebug("%d: %s", m_intPort, "opened"); // 调试信息 } else { emit send_scan_signal(m_intPort, false); // 发送信号,表示端口关闭 qDebug("%d: %s", m_intPort, "closed"); }
}
2. 修改 MainWindow 类以使用线程池
在 MainWindow
类中,我们将使用 QThreadPool
来管理 ScanTask
的实例,并在扫描按钮点击事件中提交任务。
MainWindow.h
#ifndef MAINWINDOW_H
#define MAINWINDOW_H #include <QMessageBox>
#include <QTreeWidget>
#include <QMainWindow>
#include <QThreadPool>
#include "scantask.h" QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE class MainWindow : public QMainWindow
{ Q_OBJECT
public: MainWindow(QWidget *parent = nullptr); ~MainWindow();
protected:
private: Ui::MainWindow *ui; QTreeWidgetItem *itemRoot; // 树形控件的条目,用于表示目标 IP QTreeWidgetItem *itemLeaf; // 树形控件的条目,用于表示目标端口 QThreadPool *threadpool; // 声明一个线程池 bool isPaused; // 控制暂停状态
private slots: void on_pushButton_Scan_clicked(); void on_stopButton_clicked(); void on_pushButton_Quit_clicked(); void recv_result(int port, bool isOpen); // 自定义槽函数,用于接收线程发送的消息 void on_pushButton_Stop_clicked(); // 暂停按钮点击事件 void on_pushButton_Continue_clicked(); // 继续按钮点击事件
}; #endif // MAINWINDOW_H
MainWindow.cpp
#include "mainwindow.h"
#include "ui_mainwindow.h" MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) , ui(new Ui::MainWindow)
{ ui->setupUi(this); // 给 QTreeWidget 初始化表头 QStringList head; head << "扫描结果"; ui->treeWidget->setHeaderLabels(head); threadpool = new QThreadPool; // 创建线程池 threadpool->setMaxThreadCount(10); // 设置最大线程数 isPaused = false; // 初始化暂停状态
} MainWindow::~MainWindow()
{ delete ui;
} void MainWindow::on_pushButton_Scan_clicked()
{ ui->pushButton_Scan->setEnabled(false); // 正在扫描时,禁用扫描按钮 QString strIP = ui->lineEdit_IP->text(); // 获取用户输入的 IP 地址 int startPort = ui->spinBox_Port->value(); // 获取用户输入的起始端口 int endPort = startPort + 10; // 假设扫描 10 个端口 // 验证 IP 地址是否为空 if (strIP.isEmpty()) { QMessageBox::information(this, "Error", "请输入 IP", QMessageBox::Ok); return; // 退出函数 } ui->treeWidget->clear(); // 清空 QTreeWidget 中的先前结果 itemRoot = new QTreeWidgetItem(ui->treeWidget, QStringList(strIP)); // 创建根节点,显示 IP 地址 for (int i = startPort; i < endPort; i++) { // 扫描指定范围的端口 ScanTask *task = new ScanTask(strIP, i); // 创建新的扫描任务 connect(task, &ScanTask::send_scan_signal, this, &MainWindow::recv_result); // 连接信号 threadpool->start(task); // 将任务提交到线程池 QThread::msleep(10); // 短暂休眠,避免过快提交任务 }
} void MainWindow::recv_result(int port, bool isOpen)
{ QString strPort = QString::number(port); // 将端口号转换为字符串 // 根据端口状态创建叶子节点 if (isOpen) itemLeaf = new QTreeWidgetItem(itemRoot, QStringList(strPort + " opened")); // 端口打开 else itemLeaf = new QTreeWidgetItem(itemRoot, QStringList(strPort + " closed")); // 端口关闭 ui->treeWidget->expandAll(); // 展开所有项
} void MainWindow::on_stopButton_clicked()
{ // 停止所有正在运行的任务 threadpool->clear(); // 清理线程池中未执行完的任务 ui->pushButton_Scan->setEnabled(true); // 重新启用扫描按钮
} void MainWindow::on_pushButton_Quit_clicked()
{ QApplication::quit(); // 退出应用程序
} void MainWindow::on_pushButton_Stop_clicked()
{ isPaused = true; // 设置为暂停状态 ui->pushButton_Stop->setEnabled(false); // 禁用暂停按钮 ui->pushButton_Continue->setVisible(true); // 显示继续按钮
} void MainWindow::on_pushButton_Continue_clicked()
{ isPaused = false; // 设置为继续状态 ui->pushButton_Stop->setEnabled(true); // 启用暂停按钮 ui->pushButton_Continue->setVisible(false); // 隐藏继续按钮
}
原理记录
信号发出和槽的调用
- 当
ScanTask
对象(task
)在其run()
方法中完成端口扫描后,会发出send_scan_signal
信号,传递端口号和状态(打开或关闭)。 - 由于
send_scan_signal
信号与recv_result
槽函数之间建立了连接,Qt 会自动调用recv_result
槽函数,并将信号中传递的参数(端口号和状态)传递给它。
线程池的自带功能
- 自动管理线程:
QThreadPool
会根据当前正在运行的线程数和最大线程数的设置,自动管理线程的创建和销毁。当您将任务提交到线程池时,线程池会检查当前的线程使用情况。 - 启动新线程: 如果当前运行的线程数少于您设置的最大线程数(在您的代码中是 10),线程池会自动启动新的线程来执行提交的任务。
- 任务调度: 线程池会根据任务的提交顺序和线程的可用性来调度任务的执行。它会尽量保持线程的高效利用,同时避免过多的线程导致资源竞争和上下文切换的开销。