Note
本篇讲解 Qt 的多线程与网络编程:为什么 UI 线程不能干重活、QThread 的两种用法(继承 run 与 moveToThread+worker)、跨线程信号槽与线程亲和性,最后入门 TCP 回显与 HTTP GET。适合写过界面但遇到”程序卡死”的初学者。
为什么 UI 线程不能干重活
QApplication::exec() 启动的事件循环运行在**主线程(UI 线程)**上。所有界面重绘、鼠标键盘响应、定时器,都排在这一个队列里依次处理。如果你在主线程里执行一个耗时 5 秒的任务——读大文件、解码图片、死循环计算——事件循环就被卡住了 5 秒:按钮点不动、窗口拖不动、界面变白,Windows 甚至会弹出”未响应”。
结论很简单:凡是预计超过几十毫秒的工作,都必须移出 UI 线程。Qt 提供 QThread 来创建工作线程,把重活放进去,UI 线程继续专心刷新界面。
Warning
只有 QObject 派生类才能使用信号槽与 QThread 配套机制。而且界面控件(QWidget 及其子类)只能在主线程创建和操作——子线程里直接 setText、show 都是未定义行为。子线程算好结果后,用信号槽把数据送回主线程更新界面。
QThread 用法一:继承并重写 run()
最直接的方式:写一个 QThread 子类,把任务放进 run()。
// worker.h
#ifndef WORKER_H
#define WORKER_H
#include <QThread>
class RunThread : public QThread
{
Q_OBJECT
public:
explicit RunThread(QObject *parent = nullptr) : QThread(parent) {}
signals:
// 自定义信号:把进度汇报出去
void progressChanged(int percent);
protected:
void run() override // 重写 run():线程启动后从这里开始执行
{
for (int i = 1; i <= 100; ++i) {
// 模拟耗时操作(实际可能是读文件、计算等)
QThread::msleep(20);
emit progressChanged(i); // 发射信号(run 在子线程中执行)
}
}
};
#endif // WORKER_H// 使用:在主窗口里
RunThread *thread = new RunThread(this);
connect(thread, &RunThread::progressChanged,
ui->progressBar, &QProgressBar::setValue);
thread->start(); // 启动后 run() 在新的线程里运行这种方式简单直接,但 run() 里只能执行”写死”的任务,灵活性差,也不便复用。run() 内的代码运行在新线程,其余成员函数仍运行在创建它的线程,混用时容易踩坑。
QThread 用法二:moveToThread + worker 对象(推荐)
更符合 Qt 设计思想的方式:任务逻辑写在一个普通 QObject(worker)里,然后把它搬到工作线程去。QThread 只当”线程容器”,worker 的槽函数会在那个线程里执行。
// worker.h
#ifndef WORKER_H
#define WORKER_H
#include <QObject>
class Worker : public QObject
{
Q_OBJECT
public:
explicit Worker(QObject *parent = nullptr) : QObject(parent) {}
public slots:
// 真正的耗时任务写在这里,它会运行在工作线程
void doWork(int total)
{
for (int i = 1; i <= total; ++i) {
QThread::msleep(20); // 模拟耗时
emit progressChanged(i); // 通过信号把结果送回主线程
}
emit workFinished();
}
signals:
void progressChanged(int percent);
void workFinished();
};
#endif // WORKER_H// 使用:在主窗口(或 main)里组装
#include <QThread>
#include "worker.h"
void setupWorker(QProgressBar *bar)
{
auto *thread = new QThread; // 线程容器
auto *worker = new Worker; // 任务对象(不能有父对象!)
worker->moveToThread(thread); // 把 worker 搬进子线程
// 任务结束后收尾:worker 删除、线程退出
QObject::connect(worker, &Worker::workFinished, thread, &QThread::quit);
QObject::connect(thread, &QThread::finished, worker, &QObject::deleteLater);
// 启动任务
QObject::connect(thread, &QThread::started, [worker](){ worker->doWork(100); });
// 进度更新(跨线程信号槽,见下节)
QObject::connect(worker, &Worker::progressChanged, bar, &QProgressBar::setValue);
thread->start();
}Warning
moveToThread 之前 worker 不能有 parent(QObject 的父子和线程归属绑定,有父亲的子对象无法单独搬家)。同理,主窗口里的控件不能 moveToThread。另外,必须通过信号槽或 QMetaObject::invokeMethod 调用 worker 的槽函数才会执行在子线程——直接
worker->doWork()这样调用,代码还是跑在调用者所在的线程。
跨线程信号槽与线程亲和性
每个 QObject 都有一个”所属线程”,称为线程亲和性(thread affinity):默认是创建它的那个线程,moveToThread 可以改变它(用 thread() 函数可查询)。
跨线程使用信号槽时,Qt 会根据发送者与接收者的线程亲和性自动选择连接方式,这就是第五个参数 Qt::ConnectionType 的默认值 Qt::AutoConnection:
| 连接类型 | 行为 |
|---|---|
| Qt::AutoConnection(默认) | 同线程直接调用;跨线程自动转为队列连接 |
| Qt::DirectConnection | 立即在发送者线程执行槽函数(危险,慎用) |
| Qt::QueuedConnection | 事件投递到接收者线程的事件队列,等它的事件循环来执行 |
队列连接的原理:emit 信号时,参数被复制打包成一条”事件”,塞进接收者所属线程的事件队列。所以 worker 里 emit progressChanged(i) 虽然发生在子线程,接收者 progressBar 的 setValue 却在主线程里安全执行——这正是跨线程更新界面的正确姿势。
Warning
队列连接要求信号参数类型能被 Qt 复制打包。自定义类型必须先用
qRegisterMetaType<MyType>("MyType")注册,否则运行时报警告且槽不会被调用。另外接收者线程必须有在跑的事件循环(QThread::start 后默认就有)。
Success
记住黄金组合:worker 放子线程,UI 更新走信号槽,跨线程交给 AutoConnection 自动排队。不要在子线程碰任何 QWidget,不要用 DirectConnection 更新界面。
网络入门:TCP 回显与 HTTP GET
Qt 的网络类在 .pro 里需要加 QT += network(CMake 里 find_package(Qt6 REQUIRED COMPONENTS Network) 并链接 Qt6::Network)。TCP 通信的两大主角:QTcpServer 监听端口,QTcpSocket 建立连接收发数据。下面是一个最小回显示例——客户端发什么,服务器原样返回。
服务器端(监听 + 回显):
// server.cpp —— QTcpServer 最小回显服务器
#include <QtNetwork>
#include <QTcpServer>
#include <QTcpSocket>
#include <QDebug>
class EchoServer : public QObject
{
Q_OBJECT
public:
explicit EchoServer(QObject *parent = nullptr) : QObject(parent)
{
// 监听本机 8888 端口,任何地址均可连入
if (!m_server.listen(QHostAddress::Any, 8888)) {
qDebug() << "监听失败:" << m_server.errorString();
return;
}
qDebug() << "服务器已启动,端口 8888";
// 有新客户端连入时触发
connect(&m_server, &QTcpServer::newConnection, this, &EchoServer::onNewConnection);
}
private slots:
void onNewConnection()
{
QTcpSocket *client = m_server.nextPendingConnection(); // 取出客户端套接字
connect(client, &QTcpSocket::readyRead, this, [client]() {
// 读出收到的数据并原样发回
QByteArray data = client->readAll();
client->write(data);
});
connect(client, &QTcpSocket::disconnected,
client, &QTcpSocket::deleteLater); // 断开后释放
}
private:
QTcpServer m_server;
};客户端(连接 + 收发):
// client.cpp —— QTcpSocket 客户端
#include <QApplication>
#include <QTcpSocket>
#include <QDebug>
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
QTcpSocket socket;
// 连接服务器(异步:发起后立即返回,结果用信号通知)
socket.connectToHost(QHostAddress::LocalHost, 8888);
// 连接成功后发送一条消息
QObject::connect(&socket, &QTcpSocket::connected, [&socket]() {
qDebug() << "已连接,发送数据";
socket.write("Hello Qt TCP");
});
// 收到数据时读取并打印(readyRead:有新数据可读)
QObject::connect(&socket, &QTcpSocket::readyRead, [&socket]() {
qDebug() << "收到回显:" << socket.readAll();
socket.disconnectFromHost(); // 演示完毕,断开
});
return app.exec();
}流程总结:服务器 listen → 客户端 connectToHost → 服务器 newConnection → 双方用 readyRead 信号读数据、write 发数据。所有网络操作都是异步的,靠信号槽驱动,天然不会阻塞界面。
HTTP 请求则更简单,用 QNetworkAccessManager 一站式搞定:
// HTTP GET 示例
#include <QNetworkAccessManager>
#include <QNetworkRequest>
#include <QNetworkReply>
#include <QUrl>
#include <QDebug>
QNetworkAccessManager manager; // 一个应用通常只建一个实例,可复用
void httpGet()
{
QNetworkRequest request(QUrl("https://httpbin.org/get"));
QNetworkReply *reply = manager.get(request); // 发起 GET,返回 reply 对象
// 请求完成时(无论成败)
QObject::connect(reply, &QNetworkReply::finished, [reply]() {
if (reply->error() == QNetworkReply::NoError) {
QByteArray body = reply->readAll(); // 响应内容
qDebug() << "HTTP 状态:" << reply->attribute(
QNetworkRequest::HttpStatusCodeAttribute).toInt();
qDebug() << "内容前 100 字节:" << body.left(100);
} else {
qDebug() << "请求失败:" << reply->errorString();
}
reply->deleteLater(); // 必须释放,manager 不接管 reply 的内存
});
}Success
QNetworkAccessManager 的 get/post/downloadProgress 信号可以方便地做下载进度条。记住
reply->deleteLater()——这是新手最容易漏的内存泄漏点。
自测:三个问题检验掌握程度
自测:三个问题检验掌握程度
- 为什么在按钮点击的槽函数里执行 5 秒的读文件操作界面会”未响应”?正确的处理思路是什么?
- moveToThread 之后,直接调用 worker 的方法为什么不建议?跨线程信号槽默认使用哪种连接方式、它如何保证接收者的线程安全?
- TCP 服务器和客户端分别靠哪个类的哪个信号得知”有新连接”和”有数据到达”?QNetworkReply 为什么必须 deleteLater?
(答案都在上文)