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 的槽函数会在那个线程里执行。

moveToThread 多线程协作示意图:worker 对象移入子线程后,UI 线程通过跨线程信号槽与其通信

// 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()——这是新手最容易漏的内存泄漏点。

自测:三个问题检验掌握程度