用 Qt 构建线程安全的通讯静态库:从线程亲和性到完整工程

一、问题的由来

在 Qt 里做一个通讯静态库,同时支持串口、TCP、UDP,一个常见的"正确做法"是:为每个通讯通道单独开一个工作线程,并让通讯对象在这个线程里完成创建、使用、销毁的全生命周期。

很多资料只会笼统地说"怕卡界面",于是初学者往往把它简化成"在某个线程里调用一下 send() 就行了"。但只要稍微深入 Qt 的线程模型就会发现,这个架构不是过度设计,而是被一组环环相扣的硬性约束逼出来的。本文先把这些知识点讲透,再给出一个可直接编译运行的完整工程。


二、核心知识点

1. 线程亲和性(Thread Affinity)

每一个 QObject 及其派生类(QSerialPort、QTcpSocket、QUdpSocket 都是 QObject 子类)都有一个"归属线程"。归属关系在对象被 new 的那一刻 确定:归属于执行 new 的线程。

线程亲和性决定了三件事:

  • 事件派发:对象的事件(定时器、网络 I/O 就绪通知等)只由所属线程的事件循环派发。

  • 信号槽连接类型 :AutoConnection(默认)在信号发出时比较"发射线程"与"接收对象归属线程",相同则直接调用,不同则自动转为排队连接。

  • 操作合法性 :QObject 不是线程安全的,跨线程直接调用其方法属于未定义行为。

可以用 QObject::thread() 查询归属线程,用 moveToThread() 更改------但通信类不能靠 moveToThread 补救,原因见下一条。

2. QSocketNotifier 与事件循环的"硬绑定"

这是最容易踩的坑。QTcpSocket、QUdpSocket 底层依赖操作系统提供的异步 I/O 通知机制,Qt 为此在内部创建了 QSocketNotifier 对象来监听套接字上的可读/可写事件。

关键在于:QSocketNotifier 在创建时与"当时的线程事件循环"绑定 ,moveToThread() 搬走的只是通信对象本身,QSocketNotifier 的注册关系并不会跟着搬。

后果是:如果 QTcpSocket 在主线程 new,随后被 moveToThread 到工作线程,那么操作系统层面的 I/O 就绪通知仍然会被派发回主线程,而搬走后的对象却在等新线程的通知------readyRead() 永远等不到,通信静默失效。

所以正确的做法只有一个:在哪个线程使用,就在哪个线程从头 new ,让 QSocketNotifier 的"出生地"与"工作地"一致。

3. 内部状态非线程安全

QTcpSocket、QUdpSocket、QSerialPort 都继承自 QIODevice。Qt 官方明确说明这类 I/O 设备不能跨线程调用 ,因为其内部缓冲区与内部状态都没有互斥锁保护。

Qt 核心开发者 Thiago Macieira 曾解释:QAbstractSocket 会通过 QSocketNotifier 自动尝试读写,如果从多个线程使用它,"你将遇到大量数据竞争条件,导致数据丢失或程序崩溃"。

这意味着,即便你试图用外部互斥锁"保护"对同一个 Socket 的调用,也无法覆盖 Qt 内部在事件循环驱动下对缓冲区的自动访问。唯一安全的并发模型是"独占":一个 Socket 对象在任意时刻只被其归属线程访问。

4. 跨线程信号槽的"排队"本质

跨线程信号槽之所以安全,是因为 AutoConnection 检测到发射线程 ≠ 接收对象归属线程时,会自动转为队列连接(QueuedConnection):

  • 槽函数不会在发射线程执行;

  • Qt 将这次调用封装成一个事件,投递到接收者所属线程的事件队列,由接收线程的事件循环稍后取出执行。

由此带来两个推论:

  • 异步性:跨线程信号槽调用是异步的,发射方不会等待槽函数完成。

  • 参数须可拷贝 :队列连接要求参数能被复制存储,自定义类型须 qRegisterMetaType() 注册。

一个危险的错误是跨线程使用 Qt::DirectConnection,这会让槽函数在发射线程中直接执行,绕过队列机制,等价于一个线程直接操作另一个线程的对象,直接触发第 3 条的数据竞争。

5. deleteLater 的线程语义

QObject 的销毁同样受线程归属约束。工作线程中创建的 Socket 及其 QSocketNotifier,若被主线程直接 delete,属于未定义行为。

正确做法是 deleteLater(),它的语义是:在当前控制流返回到对象所属线程的事件循环时,才执行删除。如果对象所在线程的事件循环没有运行,该对象会在线程结束时被销毁。

因此"对象在该线程生,就在该线程死"是管理生命周期最省心的方式。

6. 阻塞 API 的"污染"与隔离设计

即便只使用异步 API,独立线程提供的隔离性也不可替代。

waitForReadyRead()、waitForBytesWritten()、waitForConnected() 等阻塞函数会挂起调用线程。若在 GUI 主线程调用,界面冻结;在某些平台/绑定下,在次级线程调用也可能间接影响主线程的事件循环。此外 Qt 要求这类阻塞函数必须在创建 Socket 的同一线程中调用,否则会出现随机行为和调试断言。

独立工作线程把这类型行为严格限制在一个隔离的"沙箱"里:一个通道阻塞或数据处理繁重,只会影响它自己。

7. 纯虚 QObject 基类的编译单元

ICommDevice 作为纯虚 QObject 基类,可以不写 .cpp,前提是:

  • 其 .h 必须出现在 add_library / add_executable 的源文件列表中,让 CMake AUTOMOC 为其生成 moc;

  • 该头文件至少被某个 .cpp include 过,避免 moc 输出找不到符号。

否则链接时会出现 undefined reference to vtable for ICommDevice。


三、架构设计

有了上面的约束,架构就自然浮现:

text

复制代码
┌──────────────┐   信号排队回主线程   ┌─────────────────────┐
│   MainApp    │◄───────────────────│    CommWorkThread    │
│  (GUI主线程)  │                    │   (QThread + run)   │
│              │  emit sendRequest  │                      │
│              │───────────────────►│  ┌────────────────┐  │
└──────────────┘   (Queued)         │  │  ICommDevice   │  │
                                    │  │  (在工作线程    │  │
                                    │  │   new 出来)     │  │
                                    │  └───┬────┬───┬───┘   │
                                    │  Serial Tcp Udp       │
                                    └───────────────────────┘

关键设计:

关注点 方案
通讯对象创建位置 在 CommWorkThread::run() 中根据配置 new,确保 QSocketNotifier 与工作线程事件循环绑定
事件循环 run() 中调用 exec(),是 readyRead 能被派发的前提
接收方向 通讯对象信号经 DirectConnection 转发为 CommWorkThread 的信号(发射动作留在工作线程),主程序以 Auto 连接,自动排队回主线程
发送方向 主程序只 emit sendRequest();sendRequest 与通讯对象以 QueuedConnection 相连,send() 在工作线程执行
状态反馈 统一为 CommState 枚举,通过 stateChanged(int)、connected、disconnected、errorOccurred 回主线程
发送结果反馈 每次发送分配请求 ID,write() 结果经 sendFinished(reqId, bytes) 异步返回
生命周期 new、open、send、close、delete 全在 run() 内闭环;stop() 先 quit() 再 wait(),确保析构前 run() 已返回
配置驱动 构造函数接收 CommConfig,run() 中 switch 判断类型,主程序无需感知具体通讯方式

四、完整工程

4.1 目录结构

复制代码
QtCommDemo/
├── CMakeLists.txt
├── CommLib/
│   ├── CMakeLists.txt
│   ├── include/
│   │   ├── CommConfig.h
│   │   ├── ICommDevice.h
│   │   ├── SerialCommDevice.h
│   │   ├── TcpCommDevice.h
│   │   ├── UdpCommDevice.h
│   │   └── CommWorkThread.h
│   └── src/
│       ├── SerialCommDevice.cpp
│       ├── TcpCommDevice.cpp
│       ├── UdpCommDevice.cpp
│       └── CommWorkThread.cpp
└── App/
    ├── CMakeLists.txt
    ├── MainApp.h
    ├── MainApp.cpp
    └── main.cpp

4.2 根 CMakeLists.txt

复制代码
cmake_minimum_required(VERSION 3.16)
project(QtCommDemo LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_AUTOMOC ON)

find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Core Network SerialPort)
find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Core Network SerialPort)

add_subdirectory(CommLib)
add_subdirectory(App)

4.3 CommLib/CMakeLists.txt

复制代码
add_library(CommLib STATIC
    src/SerialCommDevice.cpp
    src/TcpCommDevice.cpp
    src/UdpCommDevice.cpp
    src/CommWorkThread.cpp

    include/CommConfig.h
    include/ICommDevice.h
    include/SerialCommDevice.h
    include/TcpCommDevice.h
    include/UdpCommDevice.h
    include/CommWorkThread.h
)

target_include_directories(CommLib PUBLIC
    ${CMAKE_CURRENT_SOURCE_DIR}/include
)

target_link_libraries(CommLib PUBLIC
    Qt${QT_VERSION_MAJOR}::Core
    Qt${QT_VERSION_MAJOR}::Network
    Qt${QT_VERSION_MAJOR}::SerialPort
)

4.4 include/CommConfig.h

复制代码
#pragma once

#include <QString>
#include <QtGlobal>

// 通讯方式
enum class CommType {
    Serial = 0,
    Tcp    = 1,
    Udp    = 2,
};

// 对外统一的连接状态,屏蔽底层不同通讯方式的状态差异
enum class CommState {
    Unconnected = 0,
    Connecting  = 1,
    Connected   = 2,
    Closing     = 3,
    Bound       = 4,
};

// 串口配置
struct SerialCommConfig {
    QString portName;
    int baudRate    = 9600;
    int dataBits    = 8;   // QSerialPort::Data8
    int parity      = 0;   // QSerialPort::NoParity
    int stopBits    = 1;   // QSerialPort::OneStop
    int flowControl = 0;   // QSerialPort::NoFlowControl
};

// TCP 客户端配置
struct TcpCommConfig {
    QString host = QStringLiteral("127.0.0.1");
    quint16 port = 9000;
};

// UDP 配置
struct UdpCommConfig {
    QString localAddress  = QStringLiteral("0.0.0.0");
    quint16 localPort     = 9001;
    QString remoteAddress = QStringLiteral("127.0.0.1");
    quint16 remotePort    = 9001;
};

// 综合配置
struct CommConfig {
    CommType         type = CommType::Serial;
    SerialCommConfig serial;
    TcpCommConfig    tcp;
    UdpCommConfig    udp;
};

4.5 include/ICommDevice.h

复制代码
#pragma once

#include <QObject>
#include <QByteArray>

// 通讯设备抽象基类,定义所有通讯方式共用的接口和信号
class ICommDevice : public QObject
{
    Q_OBJECT
public:
    explicit ICommDevice(QObject *parent = nullptr) : QObject(parent) {}
    ~ICommDevice() override = default;

    // 打开设备
    virtual bool   open() = 0;
    // 关闭设备
    virtual void   close() = 0;
    // 发送数据,返回写入的字节数,失败返回负值
    virtual qint64 send(const QByteArray &data) = 0;
    // 查询设备是否处于可通讯状态
    virtual bool   isOpen() const = 0;

signals:
    // 收到数据
    void dataReceived(const QByteArray &data);
    // 连接建立
    void connected();
    // 连接断开
    void disconnected();
    // 发生错误
    void errorOccurred(const QString &error);
    // 连接状态变化,值为 CommState 的整数值
    void stateChanged(int state);
};

4.6 include/SerialCommDevice.h

复制代码
#pragma once

#include "ICommDevice.h"
#include "CommConfig.h"
#include <QSerialPort>

// 串口通讯设备
class SerialCommDevice : public ICommDevice
{
    Q_OBJECT
public:
    explicit SerialCommDevice(const CommConfig &cfg, QObject *parent = nullptr);

    bool   open() override;
    void   close() override;
    qint64 send(const QByteArray &data) override;
    bool   isOpen() const override;

private slots:
    // 处理串口收到的数据
    void onReadyRead();

private:
    QSerialPort *m_serial = nullptr;
    CommConfig   m_cfg;
};

4.7 src/SerialCommDevice.cpp

复制代码
#include "SerialCommDevice.h"

SerialCommDevice::SerialCommDevice(const CommConfig &cfg, QObject *parent)
    : ICommDevice(parent)
    , m_serial(new QSerialPort(this))
    , m_cfg(cfg)
{
    connect(m_serial, &QSerialPort::readyRead,
            this, &SerialCommDevice::onReadyRead);

    connect(m_serial, &QSerialPort::errorOccurred, this,
            [this](QSerialPort::SerialPortError err) {
                if (err != QSerialPort::NoError)
                    emit errorOccurred(m_serial->errorString());
            });
}

bool SerialCommDevice::open()
{
    m_serial->setPortName(m_cfg.serial.portName);
    m_serial->setBaudRate(m_cfg.serial.baudRate);
    m_serial->setDataBits(static_cast<QSerialPort::DataBits>(m_cfg.serial.dataBits));
    m_serial->setParity(static_cast<QSerialPort::Parity>(m_cfg.serial.parity));
    m_serial->setStopBits(static_cast<QSerialPort::StopBits>(m_cfg.serial.stopBits));
    m_serial->setFlowControl(static_cast<QSerialPort::FlowControl>(m_cfg.serial.flowControl));

    if (m_serial->open(QIODevice::ReadWrite)) {
        emit stateChanged(static_cast<int>(CommState::Connected));
        emit connected();
        return true;
    }

    emit stateChanged(static_cast<int>(CommState::Unconnected));
    emit errorOccurred(m_serial->errorString());
    return false;
}

void SerialCommDevice::close()
{
    if (m_serial->isOpen()) {
        m_serial->close();
        emit stateChanged(static_cast<int>(CommState::Unconnected));
        emit disconnected();
    }
}

qint64 SerialCommDevice::send(const QByteArray &data)
{
    if (!m_serial->isOpen())
        return -1;
    return m_serial->write(data);
}

bool SerialCommDevice::isOpen() const
{
    return m_serial->isOpen();
}

void SerialCommDevice::onReadyRead()
{
    const QByteArray data = m_serial->readAll();
    if (!data.isEmpty())
        emit dataReceived(data);
}

4.8 include/TcpCommDevice.h

复制代码
#pragma once

#include "ICommDevice.h"
#include "CommConfig.h"
#include <QTcpSocket>

// TCP 客户端通讯设备
class TcpCommDevice : public ICommDevice
{
    Q_OBJECT
public:
    explicit TcpCommDevice(const CommConfig &cfg, QObject *parent = nullptr);

    bool   open() override;
    void   close() override;
    qint64 send(const QByteArray &data) override;
    bool   isOpen() const override;

private slots:
    // 处理 TCP 收到的数据
    void onReadyRead();
    // 连接建立时上报状态
    void onConnected();
    // 连接断开时上报状态
    void onDisconnected();
    // 上报套接字错误
    void onSocketError(QAbstractSocket::SocketError err);
    // 将底层状态翻译为 CommState
    void onSocketStateChanged(QAbstractSocket::SocketState state);

private:
    QTcpSocket *m_socket = nullptr;
    CommConfig  m_cfg;
};

4.9 src/TcpCommDevice.cpp

复制代码
#include "TcpCommDevice.h"

// 将 Qt 套接字状态翻译为统一的 CommState
static CommState toCommState(QAbstractSocket::SocketState s)
{
    switch (s) {
    case QAbstractSocket::UnconnectedState: return CommState::Unconnected;
    case QAbstractSocket::HostLookupState:
    case QAbstractSocket::ConnectingState:  return CommState::Connecting;
    case QAbstractSocket::ConnectedState:   return CommState::Connected;
    case QAbstractSocket::BoundState:       return CommState::Bound;
    case QAbstractSocket::ClosingState:     return CommState::Closing;
    default:                                return CommState::Unconnected;
    }
}

TcpCommDevice::TcpCommDevice(const CommConfig &cfg, QObject *parent)
    : ICommDevice(parent)
    , m_socket(new QTcpSocket(this))
    , m_cfg(cfg)
{
    connect(m_socket, &QTcpSocket::readyRead,
            this, &TcpCommDevice::onReadyRead);
    connect(m_socket, &QTcpSocket::connected,
            this, &TcpCommDevice::onConnected);
    connect(m_socket, &QTcpSocket::disconnected,
            this, &TcpCommDevice::onDisconnected);
    connect(m_socket, &QAbstractSocket::errorOccurred,
            this, &TcpCommDevice::onSocketError);
    connect(m_socket, &QAbstractSocket::stateChanged,
            this, &TcpCommDevice::onSocketStateChanged);
}

bool TcpCommDevice::open()
{
    // 异步连接,结果通过 connected / errorOccurred 通知
    m_socket->connectToHost(m_cfg.tcp.host, m_cfg.tcp.port);
    return true;
}

void TcpCommDevice::close()
{
    m_socket->disconnectFromHost();
}

qint64 TcpCommDevice::send(const QByteArray &data)
{
    if (m_socket->state() != QAbstractSocket::ConnectedState)
        return -1;
    return m_socket->write(data);
}

bool TcpCommDevice::isOpen() const
{
    return m_socket->state() == QAbstractSocket::ConnectedState;
}

void TcpCommDevice::onReadyRead()
{
    const QByteArray data = m_socket->readAll();
    if (!data.isEmpty())
        emit dataReceived(data);
}

void TcpCommDevice::onConnected()
{
    emit connected();
}

void TcpCommDevice::onDisconnected()
{
    emit disconnected();
}

void TcpCommDevice::onSocketError(QAbstractSocket::SocketError)
{
    emit errorOccurred(m_socket->errorString());
}

void TcpCommDevice::onSocketStateChanged(QAbstractSocket::SocketState state)
{
    emit stateChanged(static_cast<int>(toCommState(state)));
}

4.10 include/UdpCommDevice.h

复制代码
#pragma once

#include "ICommDevice.h"
#include "CommConfig.h"
#include <QUdpSocket>

// UDP 通讯设备
class UdpCommDevice : public ICommDevice
{
    Q_OBJECT
public:
    explicit UdpCommDevice(const CommConfig &cfg, QObject *parent = nullptr);

    bool   open() override;
    void   close() override;
    qint64 send(const QByteArray &data) override;
    bool   isOpen() const override;

private slots:
    // 处理 UDP 收到的数据报
    void onReadyRead();
    // 将底层状态翻译为 CommState
    void onSocketStateChanged(QAbstractSocket::SocketState state);

private:
    QUdpSocket *m_socket = nullptr;
    CommConfig  m_cfg;
};

4.11 src/UdpCommDevice.cpp

复制代码
#include "UdpCommDevice.h"
#include <QHostAddress>

// 将 Qt 套接字状态翻译为统一的 CommState
static CommState toCommState(QAbstractSocket::SocketState s)
{
    switch (s) {
    case QAbstractSocket::UnconnectedState: return CommState::Unconnected;
    case QAbstractSocket::HostLookupState:
    case QAbstractSocket::ConnectingState:  return CommState::Connecting;
    case QAbstractSocket::ConnectedState:   return CommState::Connected;
    case QAbstractSocket::BoundState:       return CommState::Bound;
    case QAbstractSocket::ClosingState:     return CommState::Closing;
    default:                                return CommState::Unconnected;
    }
}

UdpCommDevice::UdpCommDevice(const CommConfig &cfg, QObject *parent)
    : ICommDevice(parent)
    , m_socket(new QUdpSocket(this))
    , m_cfg(cfg)
{
    connect(m_socket, &QUdpSocket::readyRead,
            this, &UdpCommDevice::onReadyRead);
    connect(m_socket, &QAbstractSocket::stateChanged,
            this, &UdpCommDevice::onSocketStateChanged);
}

bool UdpCommDevice::open()
{
    if (!m_socket->bind(QHostAddress(m_cfg.udp.localAddress),
                        m_cfg.udp.localPort)) {
        emit errorOccurred(m_socket->errorString());
        return false;
    }
    emit connected();
    return true;
}

void UdpCommDevice::close()
{
    m_socket->close();
    emit disconnected();
}

qint64 UdpCommDevice::send(const QByteArray &data)
{
    const QHostAddress remote(m_cfg.udp.remoteAddress);
    return m_socket->writeDatagram(data, remote, m_cfg.udp.remotePort);
}

bool UdpCommDevice::isOpen() const
{
    return m_socket->state() == QAbstractSocket::BoundState;
}

void UdpCommDevice::onReadyRead()
{
    while (m_socket->hasPendingDatagrams()) {
        QByteArray buf;
        buf.resize(static_cast<int>(m_socket->pendingDatagramSize()));
        m_socket->readDatagram(buf.data(), buf.size());
        emit dataReceived(buf);
    }
}

void UdpCommDevice::onSocketStateChanged(QAbstractSocket::SocketState state)
{
    emit stateChanged(static_cast<int>(toCommState(state)));
}

4.12 include/CommWorkThread.h

复制代码
#pragma once

#include <QThread>
#include <QByteArray>
#include "CommConfig.h"

class ICommDevice;

// 通讯工作线程:在其 run() 中创建通讯对象并运行独立事件循环
class CommWorkThread : public QThread
{
    Q_OBJECT
public:
    explicit CommWorkThread(const CommConfig &cfg, QObject *parent = nullptr);
    ~CommWorkThread() override;

    // 提交一次发送请求,返回本次请求的 ID
    quint64 sendData(const QByteArray &data);
    // 请求退出工作线程事件循环并等待线程结束
    void    stop();

signals:
    // 收到数据
    void dataReceived(const QByteArray &data);
    // 连接建立
    void connected();
    // 连接断开
    void disconnected();
    // 发生错误
    void errorOccurred(const QString &error);
    // 设备创建并尝试打开的结果
    void deviceOpened(bool ok);
    // 连接状态变化
    void stateChanged(int state);
    // 发送完成,bytes 为写入字节数,负值表示失败
    void sendFinished(quint64 requestId, qint64 bytes);

    // 内部信号:主线程投递到工作线程的发送请求
    void sendRequest(quint64 requestId, const QByteArray &data);

protected:
    void run() override;

private:
    CommConfig   m_cfg;
    ICommDevice *m_device = nullptr;
    quint64      m_reqSeq = 0;
};

4.13 src/CommWorkThread.cpp

复制代码
#include "CommWorkThread.h"
#include "ICommDevice.h"
#include "SerialCommDevice.h"
#include "TcpCommDevice.h"
#include "UdpCommDevice.h"

CommWorkThread::CommWorkThread(const CommConfig &cfg, QObject *parent)
    : QThread(parent)
    , m_cfg(cfg)
{
}

CommWorkThread::~CommWorkThread()
{
    stop();
}

quint64 CommWorkThread::sendData(const QByteArray &data)
{
    const quint64 id = ++m_reqSeq;
    emit sendRequest(id, data);
    return id;
}

void CommWorkThread::stop()
{
    if (isRunning()) {
        quit();
        wait();
    }
}

void CommWorkThread::run()
{
    // 在工作线程中创建通讯对象,确保内部 QSocketNotifier
    // 与工作线程的事件循环绑定
    switch (m_cfg.type) {
    case CommType::Serial:
        m_device = new SerialCommDevice(m_cfg);
        break;
    case CommType::Tcp:
        m_device = new TcpCommDevice(m_cfg);
        break;
    case CommType::Udp:
        m_device = new UdpCommDevice(m_cfg);
        break;
    }

    if (!m_device) {
        emit deviceOpened(false);
        return;
    }

    // 直接连接:转发动作在工作线程中执行,
    // 主程序连接这些信号时因为线程不同会自动排队回主线程
    connect(m_device, &ICommDevice::dataReceived,
            this, &CommWorkThread::dataReceived, Qt::DirectConnection);
    connect(m_device, &ICommDevice::connected,
            this, &CommWorkThread::connected, Qt::DirectConnection);
    connect(m_device, &ICommDevice::disconnected,
            this, &CommWorkThread::disconnected, Qt::DirectConnection);
    connect(m_device, &ICommDevice::errorOccurred,
            this, &CommWorkThread::errorOccurred, Qt::DirectConnection);
    connect(m_device, &ICommDevice::stateChanged,
            this, &CommWorkThread::stateChanged, Qt::DirectConnection);

    // 队列连接:发送请求由工作线程串行执行,回发结果
    ICommDevice *dev = m_device;
    connect(this, &CommWorkThread::sendRequest, dev,
            [dev, this](quint64 reqId, const QByteArray &data) {
                const qint64 n = dev->send(data);
                emit sendFinished(reqId, n);
            },
            Qt::QueuedConnection);

    const bool ok = m_device->open();
    emit deviceOpened(ok);

    if (ok)
        exec();

    // exec() 返回后仍在工作线程上下文,安全关闭并销毁设备
    if (m_device) {
        m_device->close();
        delete m_device;
        m_device = nullptr;
    }
}

4.14 App/CMakeLists.txt

复制代码
add_executable(CommDemo
    main.cpp
    MainApp.h
    MainApp.cpp
)

target_link_libraries(CommDemo PRIVATE
    CommLib
    Qt${QT_VERSION_MAJOR}::Core
    Qt${QT_VERSION_MAJOR}::Network
    Qt${QT_VERSION_MAJOR}::SerialPort
)

4.15 App/MainApp.h

复制代码
#pragma once

#include <QObject>
#include "CommConfig.h"

class CommWorkThread;

// 主程序侧封装,负责与工作线程交互并处理所有反馈
class MainApp : public QObject
{
    Q_OBJECT
public:
    explicit MainApp(const CommConfig &cfg, QObject *parent = nullptr);
    ~MainApp() override;

    // 提交一次发送,返回请求 ID
    quint64 send(const QByteArray &data);

private slots:
    // 处理设备创建结果
    void onDeviceOpened(bool ok);
    // 处理连接建立
    void onConnected();
    // 处理连接断开
    void onDisconnected();
    // 处理连接状态变化
    void onStateChanged(int state);
    // 处理接收到的数据
    void onDataReceived(const QByteArray &data);
    // 处理发送完成
    void onSendFinished(quint64 requestId, qint64 bytes);
    // 处理错误
    void onError(const QString &error);

private:
    CommWorkThread *m_worker = nullptr;
};

4.16 App/MainApp.cpp

复制代码
#include "MainApp.h"
#include "CommWorkThread.h"
#include <QDebug>

MainApp::MainApp(const CommConfig &cfg, QObject *parent)
    : QObject(parent)
    , m_worker(new CommWorkThread(cfg, this))
{
    connect(m_worker, &CommWorkThread::deviceOpened,
            this, &MainApp::onDeviceOpened);
    connect(m_worker, &CommWorkThread::connected,
            this, &MainApp::onConnected);
    connect(m_worker, &CommWorkThread::disconnected,
            this, &MainApp::onDisconnected);
    connect(m_worker, &CommWorkThread::stateChanged,
            this, &MainApp::onStateChanged);
    connect(m_worker, &CommWorkThread::dataReceived,
            this, &MainApp::onDataReceived);
    connect(m_worker, &CommWorkThread::sendFinished,
            this, &MainApp::onSendFinished);
    connect(m_worker, &CommWorkThread::errorOccurred,
            this, &MainApp::onError);

    m_worker->start();
}

MainApp::~MainApp()
{
    m_worker->stop();
}

quint64 MainApp::send(const QByteArray &data)
{
    return m_worker->sendData(data);
}

void MainApp::onDeviceOpened(bool ok)
{
    qInfo() << "[Main]" << (ok ? "device opened" : "device open failed");
}

void MainApp::onConnected()
{
    qInfo() << "[Main] connected";
}

void MainApp::onDisconnected()
{
    qInfo() << "[Main] disconnected";
}

void MainApp::onStateChanged(int state)
{
    qInfo() << "[Main] state changed:" << state;
}

void MainApp::onDataReceived(const QByteArray &data)
{
    qInfo() << "[Main] recv:" << data;
}

void MainApp::onSendFinished(quint64 requestId, qint64 bytes)
{
    if (bytes < 0)
        qWarning() << "[Main] send failed, reqId =" << requestId;
    else
        qInfo() << "[Main] send ok, reqId =" << requestId
                << "bytes =" << bytes;
}

void MainApp::onError(const QString &error)
{
    qWarning() << "[Main] error:" << error;
}

4.17 App/main.cpp

复制代码
#include <QCoreApplication>
#include <QTimer>
#include "MainApp.h"

int main(int argc, char *argv[])
{
    QCoreApplication app(argc, argv);

    CommConfig cfg;
    cfg.type = CommType::Udp;
    cfg.udp.localAddress  = QStringLiteral("0.0.0.0");
    cfg.udp.localPort     = 9001;
    cfg.udp.remoteAddress = QStringLiteral("127.0.0.1");
    cfg.udp.remotePort    = 9001;

    MainApp demo(cfg);

    QTimer::singleShot(1500, &demo, [&demo] {
        const quint64 id = demo.send(QByteArrayLiteral("hello from main"));
        qInfo() << "[Main] queued send, reqId =" << id;
    });

    return app.exec();
}

切换通讯方式只需改 cfg.type 与对应子结构体,其余代码无需改动。


五、整体反馈链路一览

方向 信号 说明
设备创建 deviceOpened(bool) open() 的返回值
连接状态 connected / disconnected / stateChanged(int) 三种通讯统一为 CommState
接收 dataReceived(QByteArray) 已排队回主线程
发送 sendFinished(reqId, bytes) write() 的结果,负值表示失败
错误 errorOccurred(QString) 串口与网络统一

所有信号都是从工作线程发出,经 AutoConnection 排队回到主线程执行。主线程零阻塞,工作线程内部对通讯对象的访问完全串行化,符合 Qt 的线程模型约束。


六、小结

为串口、TCP、UDP 各自开启独立工作线程,并让通信对象的创建、使用、销毁在同一线程内闭环,根源在于:

  • 线程亲和性规定了对象的归属线程;

  • QSocketNotifier 与事件循环的硬绑定 要求对象必须在工作线程中诞生,moveToThread 无法补救;

  • 内部状态非线程安全要求独占访问;

  • 队列连接划分了跨线程交互的安全边界;

  • deleteLater 语义规定了对象必须在其归属线程中销毁;

  • 阻塞 API 的潜在全局影响要求必须进行故障隔离。

这几条约束共同指向同一个结论:通信对象必须在该线程生,在该线程死。理解了这一点,就理解了 Qt 多线程通讯架构的全部设计动机。

相关推荐
2501_915921431 小时前
KXBuild CI 实操,Windows/Linux 服务器接入 Jenkins 打包 iOS IPA
服务器·windows·vscode·ios·ci/cd·个人开发·敏捷流程
xiaoye-duck1 小时前
《C++ 项目:手写大模型接入 SDK》DeepSeek接口设计(上):初始化和全量响应
开发语言·网络·c++·语言模型
fpcc1 小时前
跟我学c++中级篇—streambuf在glog中的应用分析
开发语言·c++
Halo10132 小时前
VSCode设置护眼背景色
ide·vscode·编辑器
码匠许师傅2 小时前
【C++三方组件】OpenCV:计算机视觉的事实标准
c++
飞浪纪元[FWC–FE]2 小时前
Qt 构建排错实录:从 MSB 1009 到生成器选型
开发语言·数据库·qt
C^h11 小时前
c++ string
开发语言·c++
听风说ml11 小时前
socket编程 【C语言】
c语言·udp·tcp·socket编程
raindayinrain11 小时前
c++高性能--原子操作,无锁编程,操作顺序
开发语言·c++·性能优化