Qt 多线程 HTTP 下载实战:线程池、限速与进度显示
- 2026-10-14 06:31:25

Qt 多线程 HTTP 下载实战:线程池、限速与进度显示
在编程领域,多线程是提高程序执行效率和响应能力的一种常见技术,特别是在处理 I/O 密集型任务如网络下载时。Qt 是一个强大的跨平台应用程序开发框架,提供了丰富的 API 来支持多线程编程。本篇文章将深入讲解如何使用 Qt 的线程池实现多线程 HTTP 下载,并涵盖限速下载及进度显示的相关知识。
一、Qt 线程池(QThreadPool)机制
QThreadPool 是 Qt 提供的一个管理线程资源的类,它可以调度和执行异步任务,而无需程序员手动创建和管理线程。通过使用线程池,我们可以有效地避免频繁地创建和销毁线程带来的开销,同时确保系统资源的合理利用。
核心 API:
QThreadPool::globalInstance() | |
QThreadPool::start(QRunnable*) | |
QThreadPool::setMaxThreadCount(int) | |
QThreadPool::waitForDone() |
二、实现多线程 HTTP 下载的关键步骤
1. 创建线程任务(继承 QRunnable)
定义一个继承自 QRunnable 的类,用于执行下载任务。这个类应包含下载 URL、目标保存路径等信息,以及执行下载的具体逻辑,比如使用 QNetworkAccessManager 发起 HTTP 请求并接收数据。
需要注意的是:**QNetworkAccessManager 必须在有事件循环的线程中运行**。因此 QRunnable 中需要自行创建一个 QEventLoop。
2. 使用线程池
在主线程中,实例化 QThreadPool,然后将创建的下载任务对象提交到线程池。
3. 限速下载
Qt 的 QNetworkAccessManager 本身没有直接限速接口,通常有两种思路:
通过 QNetworkReply::setReadBufferSize()限制缓冲区大小,间接影响读取速度;更精确的方式是:在 readyRead信号中读取数据后,根据目标速率计算应休眠的时间,用QThread::msleep()控制节奏。
4. 设置网络上限(全局)
为了限制所有下载任务的总网络带宽,可以维护一个全局变量记录所有线程的总下载速度,在每个线程下载过程中实时更新。当达到设定的网络上限时,暂停或减缓某些线程的下载速度。
5. 单个线程限速
对于单个线程的下载速度限制,可以在每个下载任务中监控其下载速率,当超过设定值时,适当延迟数据读取,或分块下载以控制速度。
6. 实时显示下载速度
在主线程中,可以通过定时器定期查询各个线程的下载进度,计算出当前的平均下载速度,并在界面上进行展示。Qt 的信号槽机制天然适合这种跨线程通信。
三、完整代码示例
下面给出一个可运行的示例,包含主窗口、下载任务、限速逻辑和进度显示。
1. 下载任务类 DownloadTask
// downloadtask.h
#ifndef DOWNLOADTASK_H
#define DOWNLOADTASK_H
#include<QRunnable>
#include<QObject>
#include<QUrl>
#include<QString>
#include<atomic>
classQNetworkAccessManager;
classQNetworkReply;
classQFile;
// 全局下载速率统计(字节/秒),供所有任务更新
structGlobalRate {
staticstd::atomic<qint64> totalBytes; // 累计字节
staticstd::atomic<int> activeTasks; // 活跃任务数
};
classDownloadTask :public QObject, public QRunnable
{
Q_OBJECT
public:
DownloadTask(const QUrl &url,
const QString &savePath,
qint64 maxBytesPerSec = 0, // 单任务限速,0 表示不限
QObject *parent = nullptr);
voidrun()override;
signals:
voidprogress(qint64 received, qint64 total);
voidfinished(const QString &savePath, bool ok, const QString &err);
voidspeed(qint64 bytesPerSec);
private:
QUrl m_url;
QString m_savePath;
qint64 m_maxBytesPerSec; // 单线程限速
std::atomic<bool> m_abort{false};
voidthrottle(qint64 chunkBytes, qint64 elapsedMs);
};
#endif// DOWNLOADTASK_H
// downloadtask.cpp
#include"downloadtask.h"
#include<QNetworkAccessManager>
#include<QNetworkRequest>
#include<QNetworkReply>
#include<QEventLoop>
#include<QFile>
#include<QTimer>
#include<QThread>
#include<QElapsedTimer>
#include<QDebug>
std::atomic<qint64> GlobalRate::totalBytes{0};
std::atomic<int> GlobalRate::activeTasks{0};
DownloadTask::DownloadTask(const QUrl &url,
const QString &savePath,
qint64 maxBytesPerSec,
QObject *parent)
: QObject(parent)
, m_url(url)
, m_savePath(savePath)
, m_maxBytesPerSec(maxBytesPerSec)
{
setAutoDelete(true); // 任务结束后由线程池自动删除
}
voidDownloadTask::run()
{
// 每个 runnable 在自己的线程中运行,需要独立的 QNAM 和事件循环
QNetworkAccessManager nam;
QNetworkRequest req(m_url);
req.setAttribute(QNetworkRequest::RedirectPolicyAttribute,
QNetworkRequest::NoLessSafeRedirectPolicy);
QNetworkReply *reply = nam.get(req);
// 通过信号槽连接(需 QEventLoop 驱动)
QEventLoop loop;
QFile file(m_savePath);
if (!file.open(QIODevice::WriteOnly)) {
emit finished(m_savePath, false, "无法打开文件: " + file.errorString());
reply->deleteLater();
return;
}
qint64 totalBytes = -1;
qint64 receivedBytes = 0;
// 用于限速:记录窗口内接收的字节与时间
QElapsedTimer timer;
timer.start();
qint64 windowBytes = 0;
qint64 windowStart = 0;
GlobalRate::activeTasks.fetch_add(1);
QObject::connect(reply, &QNetworkReply::readyRead, [&]() {
QByteArray chunk = reply->readAll();
if (chunk.isEmpty()) return;
file.write(chunk);
receivedBytes += chunk.size();
windowBytes += chunk.size();
GlobalRate::totalBytes.fetch_add(chunk.size());
// 单任务限速:每积攒约 64KB 检查一次
if (m_maxBytesPerSec > 0 && windowBytes >= 64 * 1024) {
qint64 elapsed = timer.elapsed() - windowStart;
qint64 expected = windowBytes * 1000 / m_maxBytesPerSec; // 应耗时 ms
if (elapsed < expected) {
QThread::msleep(static_cast<unsignedlong>(expected - elapsed));
}
windowBytes = 0;
windowStart = timer.elapsed();
}
emit progress(receivedBytes, totalBytes);
});
// 定时上报瞬时速度
QTimer speedTimer;
QObject::connect(&speedTimer, &QTimer::timeout, [&]() {
static qint64 lastBytes = 0;
static qint64 lastMs = 0;
qint64 now = timer.elapsed();
qint64 cur = receivedBytes;
if (now > lastMs) {
qint64 bps = (cur - lastBytes) * 1000 / (now - lastMs);
emit speed(bps);
lastBytes = cur;
lastMs = now;
}
});
speedTimer.start(500);
// 初始总大小
QObject::connect(reply, &QNetworkReply::downloadProgress,
[&](qint64 rec, qint64 tot) {
totalBytes = tot;
emit progress(rec, tot);
});
QObject::connect(reply, &QNetworkReply::finished, [&]() {
speedTimer.stop();
loop.quit();
});
loop.exec(); // 阻塞当前线程,直到下载完成
bool ok = (reply->error() == QNetworkReply::NoError);
QString err = ok ? QString() : reply->errorString();
file.close();
reply->deleteLater();
GlobalRate::activeTasks.fetch_sub(1);
emit finished(m_savePath, ok, err);
}
2. 下载管理器(提交任务到线程池)
// downloadmanager.h
#ifndef DOWNLOADMANAGER_H
#define DOWNLOADMANAGER_H
#include<QObject>
#include<QThreadPool>
#include<QList>
classDownloadTask;
classDownloadManager :public QObject
{
Q_OBJECT
public:
explicitDownloadManager(QObject *parent = nullptr);
~DownloadManager();
// 全局线程数上限
voidsetMaxThreads(int n){ m_pool->setMaxThreadCount(n); }
// 添加任务:单任务限速 maxBps(0 为不限)
DownloadTask* addDownload(const QString &url,
const QString &savePath,
qint64 maxBps = 0);
voidwaitForAll(){ m_pool->waitForDone(); }
signals:
voidtaskProgress(const QString &path, qint64 rec, qint64 total);
voidtaskFinished(const QString &path, bool ok, const QString &err);
voidtaskSpeed(const QString &path, qint64 bps);
private:
QThreadPool *m_pool;
QList<DownloadTask*> m_tasks;
};
#endif// DOWNLOADMANAGER_H
// downloadmanager.cpp
#include"downloadmanager.h"
#include"downloadtask.h"
DownloadManager::DownloadManager(QObject *parent)
: QObject(parent)
{
m_pool = new QThreadPool(this);
m_pool->setMaxThreadCount(4); // 默认 4 个并发线程
}
DownloadManager::~DownloadManager()
{
m_pool->waitForDone();
}
DownloadTask* DownloadManager::addDownload(const QString &url,
const QString &savePath,
qint64 maxBps)
{
auto *task = new DownloadTask(QUrl(url), savePath, maxBps, this);
connect(task, &DownloadTask::progress, this,
[this, savePath](qint64 rec, qint64 total) {
emit taskProgress(savePath, rec, total);
});
connect(task, &DownloadTask::speed, this,
[this, savePath](qint64 bps) {
emit taskSpeed(savePath, bps);
});
connect(task, &DownloadTask::finished, this,
[this, savePath](const QString &path, bool ok, const QString &err) {
emit taskFinished(savePath, ok, err);
});
m_tasks.append(task);
m_pool->start(task); // 提交到线程池执行
return task;
}
3. 主窗口(进度与速度显示)
// mainwindow.h
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include<QMainWindow>
#include<QHash>
classQTableWidget;
classQLineEdit;
classQPushButton;
classQSpinBox;
classQProgressBar;
classDownloadManager;
classMainWindow :public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = nullptr);
private slots:
voidonAddClicked();
voidonProgress(const QString &path, qint64 rec, qint64 total);
voidonSpeed(const QString &path, qint64 bps);
voidonFinished(const QString &path, bool ok, const QString &err);
private:
introwOf(const QString &path); // 根据保存路径查找行
DownloadManager *m_mgr;
QTableWidget *m_table;
QLineEdit *m_urlEdit;
QLineEdit *m_pathEdit;
QSpinBox *m_threadSpin;
QSpinBox *m_speedSpin; // 单任务限速 (KB/s)
QPushButton *m_addBtn;
};
#endif// MAINWINDOW_H
// mainwindow.cpp
#include"mainwindow.h"
#include"downloadmanager.h"
#include<QVBoxLayout>
#include<QHBoxLayout>
#include<QTableWidget>
#include<QHeaderView>
#include<QLineEdit>
#include<QPushButton>
#include<QLabel>
#include<QSpinBox>
#include<QFileDialog>
#include<QMessageBox>
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
{
m_mgr = new DownloadManager(this);
// 顶部输入区
m_urlEdit = new QLineEdit(this);
m_urlEdit->setPlaceholderText("http://example.com/file.zip");
m_pathEdit = new QLineEdit(this);
m_pathEdit->setPlaceholderText("保存路径");
auto *browseBtn = new QPushButton("浏览...", this);
connect(browseBtn, &QPushButton::clicked, this, [this]{
QString f = QFileDialog::getSaveFileName(this, "保存文件");
if (!f.isEmpty()) m_pathEdit->setText(f);
});
m_threadSpin = new QSpinBox(this);
m_threadSpin->setRange(1, 32);
m_threadSpin->setValue(4);
m_threadSpin->setPrefix("线程: ");
m_speedSpin = new QSpinBox(this);
m_speedSpin->setRange(0, 100000);
m_speedSpin->setValue(0);
m_speedSpin->setSuffix(" KB/s (0=不限)");
m_speedSpin->setPrefix("单任务限速: ");
m_addBtn = new QPushButton("添加下载", this);
auto *top = new QHBoxLayout;
top->addWidget(new QLabel("URL:", this));
top->addWidget(m_urlEdit, 3);
top->addWidget(new QLabel("路径:", this));
top->addWidget(m_pathEdit, 3);
top->addWidget(browseBtn);
top->addWidget(m_threadSpin);
top->addWidget(m_speedSpin);
top->addWidget(m_addBtn);
// 下载列表
m_table = new QTableWidget(this);
m_table->setColumnCount(4);
m_table->setHorizontalHeaderLabels({"文件", "进度", "速度", "状态"});
m_table->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch);
auto *central = new QWidget(this);
auto *v = new QVBoxLayout(central);
v->addLayout(top);
v->addWidget(m_table);
setCentralWidget(central);
resize(1000, 500);
connect(m_addBtn, &QPushButton::clicked, this, &MainWindow::onAddClicked);
// 修改线程数
connect(m_threadSpin, QOverload<int>::of(&QSpinBox::valueChanged),
this, [this](int n){ m_mgr->setMaxThreads(n); });
// 连接下载管理器信号
connect(m_mgr, &DownloadManager::taskProgress,
this, &MainWindow::onProgress);
connect(m_mgr, &DownloadManager::taskSpeed,
this, &MainWindow::onSpeed);
connect(m_mgr, &DownloadManager::taskFinished,
this, &MainWindow::onFinished);
}
voidMainWindow::onAddClicked()
{
QString url = m_urlEdit->text().trimmed();
QString path = m_pathEdit->text().trimmed();
if (url.isEmpty() || path.isEmpty()) {
QMessageBox::warning(this, "提示", "请填写 URL 和保存路径");
return;
}
qint64 bps = m_speedSpin->value() * 1024LL; // KB/s -> B/s
m_mgr->addDownload(url, path, bps);
int row = m_table->rowCount();
m_table->insertRow(row);
m_table->setItem(row, 0, new QTableWidgetItem(path));
auto *bar = new QProgressBar(this);
bar->setRange(0, 100);
bar->setValue(0);
m_table->setCellWidget(row, 1, bar);
m_table->setItem(row, 2, new QTableWidgetItem("-"));
m_table->setItem(row, 3, new QTableWidgetItem("下载中"));
}
intMainWindow::rowOf(const QString &path)
{
for (int i = 0; i < m_table->rowCount(); ++i) {
if (m_table->item(i, 0) && m_table->item(i, 0)->text() == path)
return i;
}
return-1;
}
voidMainWindow::onProgress(const QString &path, qint64 rec, qint64 total)
{
int row = rowOf(path);
if (row < 0) return;
auto *bar = qobject_cast<QProgressBar*>(m_table->cellWidget(row, 1));
if (!bar) return;
if (total > 0) {
bar->setRange(0, 100);
bar->setValue(static_cast<int>(rec * 100 / total));
} else {
bar->setRange(0, 0); // 未知总大小,转圈
}
}
voidMainWindow::onSpeed(const QString &path, qint64 bps)
{
int row = rowOf(path);
if (row < 0) return;
QString txt = QString::number(bps / 1024.0, 'f', 1) + " KB/s";
m_table->item(row, 2)->setText(txt);
}
voidMainWindow::onFinished(const QString &path, bool ok, const QString &err)
{
int row = rowOf(path);
if (row < 0) return;
m_table->item(row, 3)->setText(ok ? "完成" : ("失败: " + err));
}
4. 程序入口
// main.cpp
#include<QApplication>
#include"mainwindow.h"
intmain(int argc, char *argv[])
{
QApplication a(argc, argv);
MainWindow w;
w.show();
return a.exec();
}
四、设计要点解析
1. 关于 QRunnable 与信号槽
QRunnable 并非 QObject 子类,无法直接发出信号。示例中通过多重继承QObject + QRunnable 的方式解决了这个问题。需要注意:
setAutoDelete(true)时,任务结束后由线程池释放,信号槽连接会随对象销毁而断开;若要跨线程通信, emit时会自动使用队列连接,安全地投递到主线程。
2. 为什么每个任务要自己建 QNetworkAccessManager?
QNetworkAccessManager 内部依赖事件循环处理 I/O,且不是线程安全的。虽然官方允许在多个线程中使用同一 QNAM,但推荐每个线程一个 QNAM。因为 QRunnable 可能被调度到不同线程,所以示例在每个 run() 内部创建独立 QNAM,配合 QEventLoop 实现"同步风格"的下载逻辑。
3. 限速原理
限速的核心是"按目标速率控制单位时间内读写的字节数":
记录一个时间窗口(如从 windowStart开始);累计读到的字节 windowBytes;期望耗时 = windowBytes * 1000 / maxBytesPerSec毫秒;若实际耗时小于期望耗时,就 QThread::msleep()睡够差额。
这样既避免了把线程"卡死"在大量小 sleep 上,也能比较精确地控制速度。
4. 全局网络上限的思路
若想限制所有任务的总带宽,可以:
// 全局带宽控制器(示例)
classBandwidthLimiter {
public:
staticstd::atomic<qint64> maxTotalBps; // 上限
staticstd::atomic<qint64> currentBps; // 当前总量
};
每个任务在读取数据后更新 currentBps,如果超限,则在自己这一侧多 sleep 一会儿,直到全局速率回落。更高级的做法是使用令牌桶(Token Bucket)算法,把所有线程视作一个共享桶的消费者。
5. 进度与速度显示
进度: QNetworkReply::downloadProgress(rec, total)提供原始数据;readyRead中也可以手动累加。速度:以固定间隔(如 500ms)采样一次累计字节数,差值除以时间即瞬时速度。 跨线程通知:所有信号通过 DownloadManager转发到主线程,UI 更新只在主线程执行,避免竞态。
五、扩展方向
在实际项目(如 httpdownloadtool)中,还可以进一步扩展:
断点续传:通过 HTTP Range头请求部分数据,配合本地.part文件;多线程分块下载同一文件:将文件按字节切分,多个 DownloadTask分别下载对应区间,最后合并;暂停/恢复:在任务中增加 abort标志,配合QNetworkReply::abort()实现可控中断;任务队列与优先级:使用 QThreadPool::tryStart()和自定义任务优先级;限速算法升级:用令牌桶替代简单 sleep,让限速更加平滑; UI 完善:添加全局速度显示、剩余时间估算、失败重试等。
六、总结
Qt 的线程池和网络模块为实现高效、可控的多线程 HTTP 下载提供了便利:
QThreadPool + QRunnable 让你无需手动管理线程生命周期; QNetworkAccessManager + QEventLoop 让每个任务可以在独立线程中"同步"地完成 HTTP 请求; 信号槽机制 天然适合跨线程更新 UI,实现进度与速度显示; 限速策略 通过在数据读取路径上插入时间控制实现,可精确到单任务或全局。
通过合理设计和优化,我们可以创建一个既能充分利用系统资源,又能满足限速和进度显示需求的下载工具。这在日常的软件开发中是非常实用的技能,尤其是在处理大数据量的网络传输时。