同步方式实现异步级效率:Modbus TCP 深度方案与 Qt 实现
深度分析:为何同步不可能?为何又可能?关键瓶颈在哪?如何逼近异步性能?完整 Qt 代码实现。
零、一个反直觉的真相
大多数开发者认为:
异步 = 高性能、高并发
同步 = 低性能、阻塞、卡顿
这个认知在 Modbus 场景下是错的--或者说,过于简化了。
让我们从第一性原理出发,看看真正的瓶颈是什么。
一、性能瓶颈的本质分析
1.1 Modbus 请求的生命周期
──── 时间轴 ──────────────────────────────────────────────►
T响应
├── T请求发送 ──┤├── 网络传输 ──┤├── 设备处理 ──┤├── 网络返回 ──┤
▲ ▲ ▲
CPU空闲 CPU空闲 CPU空闲
核心观察: 在一个同步请求的完整生命周期中,CPU 绝大多数时间在空闲等待(IO 等待)。这正是异步能提高效率的根本原因--它利用这些空闲时间去处理其他任务。
1.2 Modbus 通讯各环节耗时分布(实测参考值)
| 阶段 | 局域网 (100Mbps) | 工业现场总线 | 4G/5G 远程 |
|---|---|---|---|
| 请求序列化 | ~0.01ms | ~0.01ms | ~0.01ms |
| 网络传输 (往返) | ~0.5-2ms | ~1-5ms | ~20-100ms |
| 设备处理 | ~1-20ms | ~5-50ms | ~5-50ms |
| CPU 有效占比 | < 1% | < 0.1% | < 0.01% |
关键结论: 对于 Modbus 通讯,90%-99.99% 的时间 CPU 在等待 IO。异步和同步的效率差异,本质上是对这段"空白时间"的利用方式不同。
1.3 异步的高效模式
时间 ├─ 请求A ──┤├─ 请求B ──┤├─ 请求C ──┤├─ 请求D ──┤ ← 单线程
████████░░░░████████░░░░████████░░░░████████░░░░
↑ 发送 ↑ 收A ↑ 收B ↑ 收C ↑ 收D
不等待,立即发B 不等待,立即发C ...
异步核心策略:在等待 A 的响应时,不阻塞线程,而是转而发送 B、C、D......
1.4 同步的方式也可以做到这一点
线程1 ├─── 请求A ────┤
线程2 ├─── 请求B ────┤
线程3 ├─── 请求C ────┤
线程4 ├─── 请求D ────┤
同步的核心策略: 用 N 个线程 × 各自独立阻塞 来替代异步的 1 个线程 × 轮转调度。
当 N 足够大时,流水线填满,吞吐量逼近甚至超过单线程异步。
二、专业分析:同步逼近异步效率的充分必要条件
2.1 公式推演
设:
T_req= 发送请求耗时T_resp= 等待响应耗时(IO 等待)T_proc= 收到响应后的处理耗时T_total = T_req + T_resp + T_proc= 单个请求周期
单线程异步吞吐量(理想流水线):
Async_throughput ≈ 1 / max(T_req + T_proc)
// 注:T_resp 被完全流水线化了
// 极限情况:T_proc ≈ 0 时 → 1/T_req
N 线程同步吞吐量:
Sync(N)_throughput ≈ N / T_total
令二者相等:
N / T_total = 1 / max(T_req + T_proc)
N = T_total / max(T_req + T_proc)
代入典型局域网值(T_req=0.01ms, T_resp=2ms, T_proc=0.1ms):
N = (0.01 + 2 + 0.1) / max(0.01 + 0.1) = 2.11 / 0.11 ≈ 19.2
结论:约 20 个同步线程即可在局域网环境下达到单线程异步的吞吐量。
2.2 进一步优化--多连接并行
python
N_needed = T_total / max(T_req + T_proc)
当单个 TCP 连接成为瓶颈时(如设备端按顺序处理请求),可拆分为多个连接:
多连接同步:
连接1线程 ├─── 请求A ──┤├─── 请求E ──┤
连接2线程 ├─── 请求B ──┤├─── 请求F ──┤
连接3线程 ├─── 请求C ──┤├─── 请求G ──┤
连接4线程 ├─── 请求D ──┤├─── 请求H ──┤
吞吐量 = Min(N_connections × device_throughput, N_threads / T_total)
2.3 真正的制约因素
| 因素 | 影响 | 同步方案应对策略 |
|---|---|---|
| CPU 核数 | 线程不能超过 CPU 核数太多(上下文切换开销) | 使用 N = CPU核数 × 2 的经验值 |
| 设备处理能力 | 单个设备串行处理请求的极限 | 多个 TCP 连接 + 负载均衡 |
| 网络带宽 | 带宽瓶颈(Modbus 数据量小,通常不构成问题) | 忽略 |
| 响应解析耗时 | 处理数据需要 CPU 时间 | 合并响应批量处理,减少每请求开销 |
| 线程上下文切换 | 线程过多时性能下降 | 使用 QThreadPool 自适应管理 |
2.4 决策矩阵:什么时候同步方案可行?
| 场景 | 同步线程池 | 异步IO | 推荐方案 |
|---|---|---|---|
| 单设备,局域网,< 10 Hz 轮询 | ✅ 随便写 | ✅ 随便写 | 同步单线程 |
| 单设备,局域网,100 Hz 轮询 | ✅ 3-5 线程即可 | ✅ 优秀 | 异步,或同步小线程池 |
| 多设备(5+),局域网,各 50 Hz | ✅ 线程池 | ✅ 优秀 | 异步更简洁 |
| 多设备,远程(4G/5G),延迟 > 50ms | ✅ 线程池 + 多连接 | ✅ 优秀 | 均可行 |
| 极高并发(50+ 设备) | ⚠️ 线程数过多需谨慎 | ✅ 最佳 | 异步为主,同步辅助 |
三、Qt 代码实现:高性能同步 Modbus 引擎
3.1 设计架构
┌──────────────────────────────────────────────────────┐
│ 业务调用层 (主线程) │
│ 不阻塞,通过信号槽接收结果 │
│ requestRead(addr, count) → 入队 │
└──────────────────┬───────────────────────────────────┘
│ 线程安全队列 (QQueue + QMutex)
┌──────────────────▼───────────────────────────────────┐
│ 请求调度器 (Dispatcher) │
│ 维护请求队列,分发给空闲的工作线程 │
│ 支持请求合并、优先级调度 │
└──────┬──────────────┬──────────────┬─────────────────┘
│ │ │
┌───▼───┐ ┌───▼───┐ ┌───▼───┐
│Worker1│ │Worker2│ │Worker3│ ← QThreadPool
│ 连接1 │ │ 连接2 │ │ 连接3 │ ← 各持独立连接
└───┬───┘ └───┬───┘ └───┬───┘
│ │ │
┌───▼──────────────▼──────────────▼───┐
│ Modbus TCP 设备 │
│ (支持 N 个并发连接) │
└──────────────────────────────────────┘
3.2 线程安全的请求队列
cpp
// request_queue.h
#pragma once
#include <QQueue>
#include <QMutex>
#include <QWaitCondition>
#include <QSharedPointer>
#include <QModbusDataUnit>
/// 一个 Modbus 请求的完整描述
struct ModbusRequest {
int requestId; // 唯一标识,用于关联结果
int deviceId; // 设备/从站地址 (Unit ID)
int functionCode; // 功能码 (预留,当前通过 registerType 识别)
QModbusDataUnit::RegisterType registerType; // Coils / HoldingRegisters / ...
int startAddress; // 起始地址
int count; // 数量
int priority; // 优先级 (数值越小优先级越高)
qint64 enqueueTimestamp; // 入队时间戳
// 以下用于写请求
QVector<quint16> writeValues;
bool isWriteRequest = false;
};
/// 线程安全的请求-结果队列
class ThreadSafeRequestQueue {
public:
ThreadSafeRequestQueue() = default;
/// 入队一个请求(生产者:主线程调用)
void enqueue(const ModbusRequest &req) {
QMutexLocker lock(&m_mutex);
ModbusRequest copy = req;
copy.enqueueTimestamp = QDateTime::currentMSecsSinceEpoch();
m_queue.enqueue(copy);
m_notEmpty.wakeOne(); // 唤醒一个等待的工作线程
}
/// 出队一个请求(消费者:工作线程调用)
/// 如果队列为空,阻塞等待最多 timeoutMs 毫秒
/// @return true 成功出队,false 超时
bool dequeue(ModbusRequest &out, int timeoutMs = 5000) {
QMutexLocker lock(&m_mutex);
while (m_queue.isEmpty()) {
if (!m_notEmpty.wait(&m_mutex, timeoutMs)) {
return false; // 超时
}
}
// 优先级调度:取最高优先级的请求
// 对于同优先级的,保持 FIFO
int bestIdx = 0;
int bestPriority = m_queue[0].priority;
for (int i = 1; i < m_queue.size(); ++i) {
if (m_queue[i].priority < bestPriority) {
bestPriority = m_queue[i].priority;
bestIdx = i;
}
}
out = m_queue.takeAt(bestIdx);
return true;
}
/// 批量入队(用于请求合并优化)
void enqueueBatch(const QList<ModbusRequest> &requests) {
QMutexLocker lock(&m_mutex);
for (const auto &req : requests) {
ModbusRequest copy = req;
copy.enqueueTimestamp = QDateTime::currentMSecsSinceEpoch();
m_queue.enqueue(copy);
}
m_notEmpty.wakeAll(); // 唤醒所有等待线程
}
/// 清空队列(断线时调用)
void clear() {
QMutexLocker lock(&m_mutex);
m_queue.clear();
}
int size() const {
QMutexLocker lock(&m_mutex);
return m_queue.size();
}
private:
mutable QMutex m_mutex;
QQueue<ModbusRequest> m_queue;
QWaitCondition m_notEmpty;
};
3.3 工作线程:每个线程持有独立连接,同步阻塞收发
cpp
// sync_worker.h
#pragma once
#include <QObject>
#include <QThread>
#include <QModbusTcpClient>
#include <QModbusDataUnit>
#include <QModbusReply>
#include <QElapsedTimer>
#include <QDebug>
#include "request_queue.h"
/// Modbus 同步工作线程
///
/// 核心设计思想:
/// 每个 Worker 持有一个独立的 QModbusTcpClient 连接,
/// 在独立线程中以同步方式(QEventLoop 阻塞)执行请求。
/// 多个 Worker 并行运行,达到类似异步的流水线效果。
///
class SyncModbusWorker : public QObject {
Q_OBJECT
public:
/// @param workerId 工作线程编号,用于日志
/// @param host Modbus TCP 服务器地址
/// @param port 端口 (默认 502)
/// @param timeoutMs 单次请求超时时间
/// @param retryCount 超时重试次数
explicit SyncModbusWorker(int workerId,
const QString &host,
quint16 port = 502,
int timeoutMs = 3000,
int retryCount = 2,
QObject *parent = nullptr)
: QObject(parent)
, m_workerId(workerId)
, m_host(host)
, m_port(port)
, m_timeoutMs(timeoutMs)
, m_maxRetries(retryCount)
{
// Worker 对象会被 moveToThread,因此 m_client
// 需要在工作线程的构造函数或槽函数中创建
}
~SyncModbusWorker() {
disconnectFromDevice();
}
/// 关联请求队列(必须在使用前调用)
void setQueue(std::shared_ptr<ThreadSafeRequestQueue> queue) {
m_queue = queue;
}
public slots:
/// 在工作线程中初始化连接(需通过信号槽触发,确保在正确线程执行)
bool connectToDevice() {
if (m_client) {
m_client->disconnectFromHost();
m_client->deleteLater();
m_client = nullptr;
}
m_client = new QModbusTcpClient(this);
m_client->setTimeout(m_timeoutMs);
m_client->setNumberOfRetries(m_maxRetries);
qDebug() << "[Worker" << m_workerId << "] 连接" << m_host << ":" << m_port;
return m_client->connectToHost(m_host, m_port);
}
/// 断开连接
void disconnectFromDevice() {
if (m_client) {
m_client->disconnectFromHost();
m_client->deleteLater();
m_client = nullptr;
}
}
/// 工作线程主循环--从队列取请求,同步执行,发出结果信号
///
/// 这是核心方法:在一个无限循环中不断从队列取请求,
/// 执行同步 Modbus 操作,发出结果信号。
/// 当收到停止信号时退出循环。
void startWorking() {
if (!m_client) {
emit workerError(m_workerId, "客户端未初始化");
return;
}
qDebug() << "[Worker" << m_workerId << "] 开始工作";
// 等待连接建立
if (m_client->state() != QModbusDevice::ConnectedState) {
if (!m_client->connectToHost(m_host, m_port)) {
emit workerError(m_workerId, "连接失败");
return;
}
// 同步等待连接完成(最多 3 秒)
if (!waitForConnected(3000)) {
emit workerError(m_workerId, "连接超时");
return;
}
}
m_running = true;
m_stats.reset();
while (m_running) {
// 1. 从队列取出一个请求(阻塞式,最多等 1 秒以便检查停止信号)
ModbusRequest req;
if (!m_queue || !m_queue->dequeue(req, 1000)) {
continue; // 超时重试(同时可响应停止信号)
}
// 2. 执行请求并测量耗时
m_stats.totalRequests++;
m_stats.lastRequestTime = QDateTime::currentMSecsSinceEpoch();
QElapsedTimer timer;
timer.start();
bool success = executeRequest(req);
qint64 elapsed = timer.elapsed();
m_stats.totalTime += elapsed;
if (success) {
m_stats.successCount++;
qDebug() << "[Worker" << m_workerId << "] 完成请求 #" << req.requestId
<< "耗时:" << elapsed << "ms";
} else {
m_stats.failCount++;
qWarning() << "[Worker" << m_workerId << "] 请求 #" << req.requestId << "失败";
}
}
qDebug() << "[Worker" << m_workerId << "] 停止工作";
emit workerFinished(m_workerId);
}
/// 停止工作循环
void stop() {
m_running = false;
}
signals:
/// 读取线圈的结果
/// @param workerId 工作线程编号
/// @param requestId 请求 ID
/// @param addr 起始地址
/// @param values 线圈值列表
void coilsRead(int workerId, int requestId, int addr, QVector<bool> values);
/// 读取寄存器的结果
void holdingRegistersRead(int workerId, int requestId, int addr, QVector<quint16> values);
/// 写操作完成
void writeCompleted(int workerId, int requestId, bool success);
/// 协议异常(异常码)
void protocolException(int workerId, int requestId, quint8 exceptionCode);
/// 工作线程发生错误
void workerError(int workerId, const QString &error);
/// 工作线程结束
void workerFinished(int workerId);
public:
/// 统计信息
struct WorkerStats {
qint64 totalRequests = 0;
qint64 successCount = 0;
qint64 failCount = 0;
qint64 totalTime = 0; // 所有请求总耗时 (ms)
qint64 lastRequestTime = 0; // 上次请求时间戳
void reset() {
totalRequests = 0;
successCount = 0;
failCount = 0;
totalTime = 0;
lastRequestTime = 0;
}
double avgLatencyMs() const {
return totalRequests > 0 ? (double)totalTime / totalRequests : 0.0;
}
double throughputPerSec() const {
if (totalTime == 0) return 0.0;
return (double)totalRequests / (totalTime / 1000.0);
}
};
WorkerStats stats() const { return m_stats; }
private:
/// 同步方式执行一个 Modbus 请求
/// 使用 QEventLoop 阻塞当前线程直到响应返回或超时
/// @return true 表示成功
bool executeRequest(const ModbusRequest &req) {
if (!m_client || m_client->state() != QModbusDevice::ConnectedState) {
// 断线自动重连
qWarning() << "[Worker" << m_workerId << "] 连接断开,尝试重连...";
if (!m_client || !m_client->connectToHost(m_host, m_port)) {
return false;
}
if (!waitForConnected(5000)) {
return false;
}
}
if (req.isWriteRequest) {
return executeWriteRequest(req);
} else {
return executeReadRequest(req);
}
}
/// 同步执行读请求
bool executeReadRequest(const ModbusRequest &req) {
QModbusDataUnit readUnit(req.registerType, req.startAddress, req.count);
// 发送请求
QModbusReply *reply = m_client->sendReadRequest(readUnit, req.deviceId);
if (!reply) {
return false;
}
// ─── 核心技巧:用 QEventLoop 实现同步等待 ───
// 这不比异步"低级",它只是将异步信号槽封装为同步语义。
// 在工作线程中运行,不阻塞主线程。
QEventLoop loop;
QTimer timeoutGuard;
timeoutGuard.setSingleShot(true);
// 方案 A: 收到响应 → 退出事件循环
connect(reply, &QModbusReply::finished, &loop, &QEventLoop::quit);
// 方案 B: 超时保护 → 退出事件循环
connect(&timeoutGuard, &QTimer::timeout, &loop, &QEventLoop::quit);
timeoutGuard.start(m_timeoutMs * (m_maxRetries + 1));
loop.exec(); // ★ 在此处阻塞,但 CPU 可调度其他线程
// 检查结果
bool success = false;
if (reply->isFinished() && reply->error() == QModbusDevice::NoError) {
const QModbusDataUnit result = reply->result();
success = true;
// 根据寄存器类型发出对应的信号
switch (req.registerType) {
case QModbusDataUnit::Coils: {
QVector<bool> coils;
for (quint16 v : result.values()) {
coils.append(v != 0);
}
emit coilsRead(m_workerId, req.requestId, req.startAddress, coils);
break;
}
case QModbusDataUnit::HoldingRegisters: {
QVector<quint16> regs = result.values();
emit holdingRegistersRead(m_workerId, req.requestId,
req.startAddress, regs);
break;
}
default:
break;
}
} else if (reply->isFinished() && reply->error() == QModbusDevice::ProtocolError) {
quint8 exCode = reply->rawResult().data().at(0);
emit protocolException(m_workerId, req.requestId, exCode);
}
reply->deleteLater();
return success;
}
/// 同步执行写请求
bool executeWriteRequest(const ModbusRequest &req) {
QModbusDataUnit writeUnit(req.registerType, req.startAddress,
req.writeValues.size());
for (int i = 0; i < req.writeValues.size(); ++i) {
writeUnit.setValue(i, req.writeValues[i]);
}
QModbusReply *reply = m_client->sendWriteRequest(writeUnit, req.deviceId);
if (!reply) return false;
QEventLoop loop;
QTimer timeoutGuard;
timeoutGuard.setSingleShot(true);
connect(reply, &QModbusReply::finished, &loop, &QEventLoop::quit);
connect(&timeoutGuard, &QTimer::timeout, &loop, &QEventLoop::quit);
timeoutGuard.start(m_timeoutMs * (m_maxRetries + 1));
loop.exec();
bool success = (reply->isFinished() && reply->error() == QModbusDevice::NoError);
if (!success && reply->isFinished() && reply->error() == QModbusDevice::ProtocolError) {
quint8 exCode = reply->rawResult().data().at(0);
emit protocolException(m_workerId, req.requestId, exCode);
}
reply->deleteLater();
emit writeCompleted(m_workerId, req.requestId, success);
return success;
}
/// 同步等待连接建立
bool waitForConnected(int timeoutMs) {
if (!m_client) return false;
// 通过嵌套事件循环等待连接信号
QEventLoop loop;
QTimer timer;
timer.setSingleShot(true);
connect(m_client, &QModbusClient::stateChanged, &loop, [&](QModbusDevice::State state) {
if (state == QModbusDevice::ConnectedState) {
loop.quit();
}
});
connect(&timer, &QTimer::timeout, &loop, &QEventLoop::quit);
timer.start(timeoutMs);
if (m_client->state() != QModbusDevice::ConnectedState) {
loop.exec();
}
return m_client->state() == QModbusDevice::ConnectedState;
}
int m_workerId;
QString m_host;
quint16 m_port;
int m_timeoutMs;
int m_maxRetries;
QModbusTcpClient *m_client = nullptr;
std::shared_ptr<ThreadSafeRequestQueue> m_queue;
volatile bool m_running = false;
WorkerStats m_stats;
};
3.4 线程池管理器--自动缩放的引擎
cpp
// sync_engine.h
#pragma once
#include <QObject>
#include <QThread>
#include <QList>
#include <QThreadPool>
#include <QAtomicInt>
#include <memory>
#include "request_queue.h"
#include "sync_worker.h"
/// 高性能同步 Modbus 引擎
///
/// 使用线程池 + 多连接同步阻塞 Worker 实现高吞吐量。
/// 设计目标:以同步编程的简单性,达到接近异步的性能。
///
/// 关键优化策略:
/// 1. 多线程并行(每个 Worker 一个线程,独立连接)
/// 2. 请求合并(相邻地址的读请求合并为一个大请求)
/// 3. 优先级调度(高优先级请求被优先处理)
/// 4. 自动重连(连接断开时自动恢复)
/// 5. 统计监控(实时吞吐量、延迟)
///
class SyncModbusEngine : public QObject {
Q_OBJECT
public:
/// @param host Modbus 设备地址
/// @param port TCP 端口
/// @param workerCount 工作线程数(建议 CPU 核数 × 1.5 ~ 2)
/// @param timeoutMs 单请求超时(毫秒)
explicit SyncModbusEngine(const QString &host,
quint16 port = 502,
int workerCount = 4,
int timeoutMs = 3000,
QObject *parent = nullptr)
: QObject(parent)
, m_host(host)
, m_port(port)
, m_workerCount(workerCount)
, m_timeoutMs(timeoutMs)
{
m_queue = std::make_shared<ThreadSafeRequestQueue>();
m_nextRequestId.storeRelaxed(1);
}
~SyncModbusEngine() {
stop();
}
/// 启动引擎:创建并启动所有工作线程
void start() {
if (m_running) return;
m_running = true;
// 选择最佳线程数(如果未指定)
if (m_workerCount <= 0) {
m_workerCount = QThread::idealThreadCount() * 2;
if (m_workerCount < 2) m_workerCount = 2;
if (m_workerCount > 16) m_workerCount = 16; // 上限保护
}
qDebug() << "[Engines] 启动" << m_workerCount << "个同步工作线程";
qDebug() << "[Engines] 目标设备" << m_host << ":" << m_port;
for (int i = 0; i < m_workerCount; ++i) {
auto *worker = new SyncModbusWorker(i, m_host, m_port, m_timeoutMs, 2);
worker->setQueue(m_queue);
auto *thread = new QThread(this);
worker->moveToThread(thread);
// 启动时连接
connect(thread, &QThread::started, worker, [worker]() {
worker->connectToDevice();
worker->startWorking();
});
// 清理
connect(worker, &SyncModbusWorker::workerFinished, this, [this, worker, thread](int) {
thread->quit();
thread->wait();
worker->deleteLater();
thread->deleteLater();
});
// 转发结果信号
connect(worker, &SyncModbusWorker::coilsRead,
this, &SyncModbusEngine::coilsRead);
connect(worker, &SyncModbusWorker::holdingRegistersRead,
this, &SyncModbusEngine::holdingRegistersRead);
connect(worker, &SyncModbusWorker::writeCompleted,
this, &SyncModbusEngine::writeCompleted);
connect(worker, &SyncModbusWorker::protocolException,
this, &SyncModbusEngine::protocolException);
connect(worker, &SyncModbusWorker::workerError,
this, &SyncModbusEngine::workerError);
m_workers.append(worker);
m_threads.append(thread);
thread->start();
}
}
/// 停止引擎
void stop() {
if (!m_running) return;
m_running = false;
// 停止所有工作线程
for (auto *worker : m_workers) {
worker->stop();
}
// 唤醒队列(防止线程在 dequeue 中阻塞)
// 入队一个空的停止请求
for (int i = 0; i < m_workers.size(); ++i) {
ModbusRequest stopReq;
stopReq.requestId = -1;
stopReq.priority = -1; // 最高优先级
m_queue->enqueue(stopReq);
}
// 等待线程结束
for (auto *thread : m_threads) {
thread->quit();
thread->wait(3000);
}
m_workers.clear();
m_threads.clear();
m_queue->clear();
}
// ─── 公开 API ─────────────────────────────────────
/// 读取线圈(异步风格调用,但内部由同步工作线程执行)
/// @return requestId 用于跟踪请求
int readCoils(int addr, int count, int unitId = 1, int priority = 0) {
ModbusRequest req = makeBaseRequest(unitId, priority);
req.registerType = QModbusDataUnit::Coils;
req.startAddress = addr;
req.count = count;
req.isWriteRequest = false;
m_queue->enqueue(req);
return req.requestId;
}
/// 读取保持寄存器
int readHoldingRegisters(int addr, int count, int unitId = 1, int priority = 0) {
ModbusRequest req = makeBaseRequest(unitId, priority);
req.registerType = QModbusDataUnit::HoldingRegisters;
req.startAddress = addr;
req.count = count;
req.isWriteRequest = false;
m_queue->enqueue(req);
return req.requestId;
}
/// 写单个线圈
int writeCoil(int addr, bool value, int unitId = 1, int priority = 0) {
ModbusRequest req = makeBaseRequest(unitId, priority);
req.registerType = QModbusDataUnit::Coils;
req.startAddress = addr;
req.count = 1;
req.isWriteRequest = true;
req.writeValues = { value ? 0xFF00u : 0x0000u };
m_queue->enqueue(req);
return req.requestId;
}
/// 写单个寄存器
int writeRegister(int addr, quint16 value, int unitId = 1, int priority = 0) {
ModbusRequest req = makeBaseRequest(unitId, priority);
req.registerType = QModbusDataUnit::HoldingRegisters;
req.startAddress = addr;
req.count = 1;
req.isWriteRequest = true;
req.writeValues = { value };
m_queue->enqueue(req);
return req.requestId;
}
/// 批量读取(请求合并优化)
///
/// 自动将多个连续地址的读取合并为一个大请求,减少网络开销。
/// 适用于轮询场景中读取一组连续的寄存器。
///
/// @param addrRanges {起始地址, 数量} 列表
QList<int> batchRead(const QVector<QPair<int, int>> &addrRanges,
QModbusDataUnit::RegisterType type =
QModbusDataUnit::HoldingRegisters,
int unitId = 1) {
QList<int> requestIds;
// 请求合并:将相邻的地址区间合并
// 例如:[0,5] + [5,3] → [0,8] (合并条件:endA == startB)
QVector<QPair<int, int>> merged;
auto sorted = addrRanges;
std::sort(sorted.begin(), sorted.end());
for (const auto &range : sorted) {
if (merged.isEmpty()) {
merged.append(range);
continue;
}
auto &last = merged.last();
int lastEnd = last.first + last.second;
if (lastEnd >= range.first) {
// 相邻或有重叠 → 合并
int newEnd = qMax(lastEnd, range.first + range.second);
last.second = newEnd - last.first;
// 保护:不超过单次请求上限(125 寄存器 / 2000 线圈)
if (type == QModbusDataUnit::HoldingRegisters && last.second > 125) {
// 拆分:保持前面的,后面的作为新区间
QPair<int, int> overflow(range.first + 125,
last.second - 125);
last.second = 125;
merged.append(overflow);
}
} else {
merged.append(range);
}
}
// 入队合并后的请求
QList<ModbusRequest> batchReqs;
for (const auto &range : merged) {
ModbusRequest req = makeBaseRequest(unitId, 0);
req.registerType = type;
req.startAddress = range.first;
req.count = range.second;
req.isWriteRequest = false;
batchReqs.append(req);
requestIds.append(req.requestId);
}
m_queue->enqueueBatch(batchReqs);
return requestIds;
}
/// 获取统计信息(所有 Worker 汇总)
struct EngineStats {
qint64 totalRequests = 0;
qint64 successCount = 0;
qint64 failCount = 0;
double avgLatencyMs = 0.0;
double throughputPerSec = 0.0;
int workerCount = 0;
int queueSize = 0;
QString toString() const {
return QString("Engine Stats [%1 workers]:\n"
" 总请求: %2\n"
" 成功: %3\n"
" 失败: %4\n"
" 平均延迟: %5 ms\n"
" 吞吐量: %6 req/s\n"
" 队列积压: %7\n")
.arg(workerCount)
.arg(totalRequests)
.arg(successCount)
.arg(failCount)
.arg(avgLatencyMs, 0, 'f', 2)
.arg(throughputPerSec, 0, 'f', 1)
.arg(queueSize);
}
};
EngineStats getStats() const {
EngineStats stats;
stats.workerCount = m_workers.size();
stats.queueSize = m_queue->size();
for (const auto *worker : m_workers) {
auto wStats = worker->stats();
stats.totalRequests += wStats.totalRequests;
stats.successCount += wStats.successCount;
stats.failCount += wStats.failCount;
stats.avgLatencyMs += wStats.avgLatencyMs();
stats.throughputPerSec += wStats.throughputPerSec();
}
if (m_workers.size() > 0) {
stats.avgLatencyMs /= m_workers.size();
}
return stats;
}
signals:
// 转发自工作线程的信号(对上层透明)
void coilsRead(int workerId, int requestId, int addr, QVector<bool> values);
void holdingRegistersRead(int workerId, int requestId, int addr, QVector<quint16> values);
void writeCompleted(int workerId, int requestId, bool success);
void protocolException(int workerId, int requestId, quint8 exceptionCode);
void workerError(int workerId, const QString &error);
private:
ModbusRequest makeBaseRequest(int unitId, int priority) {
ModbusRequest req;
req.requestId = m_nextRequestId.fetchAndAddRelaxed(1);
req.deviceId = unitId;
req.priority = priority;
return req;
}
QString m_host;
quint16 m_port;
int m_workerCount;
int m_timeoutMs;
volatile bool m_running = false;
std::shared_ptr<ThreadSafeRequestQueue> m_queue;
QList<SyncModbusWorker *> m_workers;
QList<QThread *> m_threads;
QAtomicInt m_nextRequestId;
};
3.5 使用示例
cpp
// main.cpp --- 完整使用示例
#include <QCoreApplication>
#include <QDebug>
#include <QTimer>
#include "sync_engine.h"
int main(int argc, char *argv[])
{
QCoreApplication app(argc, argv);
// 创建引擎(4 个工作线程,分别持有独立 TCP 连接)
SyncModbusEngine *engine = new SyncModbusEngine("192.168.1.100", 502, 4, 3000);
// 连接结果信号
QObject::connect(engine, &SyncModbusEngine::holdingRegistersRead,
[](int wid, int rid, int addr, QVector<quint16> values) {
qDebug() << "Worker" << wid << "寄存器[" << addr << "]:" << values;
});
// 启动引擎
engine->start();
// 周期性轮询(主线程定时器,不阻塞)
QTimer pollTimer;
QObject::connect(&pollTimer, &QTimer::timeout, [engine]() {
engine->readHoldingRegisters(0, 10, 1, 0); // 读 10 个寄存器
engine->readCoils(0, 16, 1, 0); // 读 16 个线圈
});
pollTimer.start(50); // 20 Hz
// 定时输出统计
QTimer statsTimer;
QObject::connect(&statsTimer, &QTimer::timeout, [engine]() {
qDebug().noquote() << engine->getStats().toString();
});
statsTimer.start(5000);
// 30 秒后退出
QTimer::singleShot(30000, &app, [engine, &app]() {
engine->stop();
app.quit();
});
return app.exec();
}
3.6 批量读取 + 请求合并
cpp
void exampleBatchRead(SyncModbusEngine *engine) {
// 读取地址 0-9、10-19、50-54
// 自动合并为 [0,20] + [50,5] → 原 3 请求变 2 请求
QVector<QPair<int, int>> ranges = {{0,10}, {10,10}, {50,5}};
QList<int> reqIds = engine->batchRead(ranges);
}
四、专业视角:同步 vs 异步的终极分析
4.1 出发点不同,终点相同
异步的出发点:"我不等,先去干别的"
同步的出发点:"我等,但让别人替我干"
结果:都是利用等待时间去处理其他请求
本质一致性:CPU 永远不闲着
4.2 11 维深度评测
| 维度 | 单线程异步 | 多线程同步(本文方案) |
|---|---|---|
| 并发模型 | 事件循环 + 回调 | 线程池 + 阻塞调用 |
| CPU 利用率 | 高(单核吃满) | 高(多核均衡) |
| IO 等待利用率 | 100%(一个线程做所有事) | N × 100%(N 个线程各自做事) |
| 代码可读性 | ❌ 回调嵌套 / 状态机 | ✅ 线性顺序代码 |
| 上下文切换 | 零(单线程) | 有(Modbus 场景可忽略) |
| 多核利用 | ❌ 单核(除非手动拆分) | ✅ 自动利用所有核心 |
| 吞吐量上限 | Min(单连接上限, CPU) | Min(N × 单连接上限, CPU) |
| 延迟 | 低(无线程切换) | 略高(队列 + 切换 < 0.1ms) |
| 调试难度 | 高(回调不易追踪) | 低(调用栈完整) |
| 资源消耗 | 低(1 线程) | 较高(N 线程 + N 连接) |
4.3 核心数学结论
令:
L = 网络延迟 (RTT)
Tp = 设备处理时间
C = 每请求 CPU 时间 (序列化/反序列化)
Nc = 设备支持的并发连接数
Nt = 同步线程数
异步单线程吞吐量:
TA_max ≈ 1 / max(C, (L + Tp) / Nc)
同步多线程吞吐量(本文方案):
TS_max ≈ Nt / (L + Tp + C)
令 TS_max >= TA_max:
Nt >= (L + Tp + C) / max(C, (L + Tp) / Nc)
4.4 何时不应该用同步多线程
- 设备只支持单个 TCP 连接 --- 多线程不增加吞吐量,反增加开销
- 极高并发(100+ 设备) --- 400 线程的上下文切换是灾难
- 设备极快 + 局域网延迟极低 (L + Tp < C) --- CPU 本身是瓶颈
五、最终结论
5.1 选型决策树
设备支持多连接?
├── 是 → 同步线程池可行,代码简单 ✅
└── 否 → 只能异步
团队擅长哪种模型?
├── 同步思维 → 线程池(本文方案)
└── 事件驱动 → 用异步
并发 > 50 设备?
├── 是 → 强制异步
└── 否 → 两者均可
代码需要长期维护?
├── 是 → 同步(调试/排错/新人上手更友好)
└── 否 → 选熟悉的
5.2 一句话
同步通过线程级并行填补 IO 等待空白,异步通过事件级轮转填补同一空白。当线程数 ≈ 设备并发连接数时,同步的吞吐量与异步等价,但代码复杂度低一个数量级。
5.3 推荐架构
┌──────────────────────────────────────┐
│ 业务逻辑层 │
│ engine->readRegs() → 信号接收结果 │
│ (完全不知道内部是同步还是异步) │
├──────────────────────────────────────┤
│ SyncModbusEngine (主线程或独立线程) │
│ ├─ 请求队列 │
│ ├─ Worker 线程池管理 │
│ └─ 请求合并优化 │
├──────────────────────────────────────┤
│ SyncModbusWorker × N (工作线程) │
│ ├─ 每个持有一个 QModbusTcpClient │
│ ├─ 内部同步阻塞执行 │
│ └─ 结果通过信号发出(跨线程自动排队) │
└──────────────────────────────────────┘
关键设计: 内部实现从同步切换到异步,甚至从 Modbus TCP 切换到 RTU------上层业务代码完全不需要修改。
本文完整实现了"同步方式获得异步级效率"的方案。核心思想不是让同步变快,而是用 N 个同步线程模拟异步的流水线效果。在绝大多数工业 Modbus 场景中,这种模式以更少的代码换取足够性能,同时显著降低维护成本。当设备不支持多连接或需 100+ 设备并发时,再回到纯异步。