☰
udp数据报接收逻辑---代码分析
2026/10/11 22:36:22 网站建设 项目流程

该函数运行在子线程中。

void UdpFuc::slot_start() { char pBuf[1500]; int ret = 0; memset((char *)pBuf, '\0', 1500); int sockAddrSize = sizeof (struct sockaddr_in); while(1) { ret = recvfrom(m_recvSocket, (char*)pBuf, sizeof(pBuf), 0, (struct sockaddr *)&m_recvSockAddr, (socklen_t*)&sockAddrSize); // 处理代码 if(ret>0) { QByteArray bdp((const char*)pBuf, ret); emit sign_bdp(bdp); } } }

每包emit的性能问题

看这段代码:

if(ret > 0) { QByteArray bdp((const char*)pBuf, ret); emit sign_bdp(bdp); }

每收到一个 UDP 包就 emit 一次。在低频场景(比如每秒几十个包)完全没问题,但在高频场景下会引发一系列问题。逐个拆开。

为什么?

一、先搞清楚emit跨线程发生了什么

假设:

  • UdpFuc对象在工作线程里运行(slot_start在工作线程)

  • sign_bdp连接的槽在主线程(GUI 线程)

则emit触发的是Qt::QueuedConnection(跨线程自动用队列连接):

工作线程 主线程 │ │ recvfrom 收到包 │ │ │ QByteArray 拷贝 │ │ │ emit sign_bdp(bdp) │ │ │ 构造一个 QMetaCallEvent ──投递──► 事件队列 │ │ 继续 recvfrom │ │ │ 事件循环取出 │ │ 调用槽函数 │ │ 处理 bdp

关键点:emit不是"直接调用槽函数",而是投递一个事件到目标线程的事件队列,然后立即返回。

二、高频下的问题

问题 1:事件队列堆积 → 内存暴涨

假设包速率10 万包/秒,每个包 1400 字节:

每秒投递 10 万个QMetaCallEvent
每个事件携带一个 QByteArray(拷贝了 1400 字节)

如果主线程处理速度跟不上(GUI 线程还要渲染、响应点击):

队列长度:10000 → 50000 → 200000 → ... 内存占用:10000 × 1400 ≈ 14 MB 100000 × 1400 ≈ 140 MB 1000000 × 1400 ≈ 1.4 GB ← 内存爆炸

后果:

  • 内存持续增长

  • 主线程越来越卡(每次要从队列取事件)

  • 最终 OOM 或界面假死

OOM是Out Of Memory的缩写,中文意思是内存耗尽 / 内存溢出。

问题 2:QByteArray的深拷贝开销

QByteArray bdp((const char*)pBuf, ret);

这一行做了:

  1. malloc分配ret字节堆内存

  2. memcpy从pBuf拷贝ret字节

  3. 构造QByteArray对象(引用计数、隐式共享结构)

10 万包/秒 × 1400 字节 =每秒 140 MB 的内存分配 + 拷贝。

加上emit投递时QByteArray还会再拷贝一次到事件里(虽然QByteArray是隐式共享的,但跨线程投递时通常会触发 detach)。

detach是 Qt 里和「隐式共享(COW)」配套的一个动作,中文常译作分离或脱离共享。

纯粹的 CPU 和内存带宽浪费。




根本原因:包速率 > 处理速率

这是一个生产者-消费者速率不匹配问题:

生产者(recvfrom):10 万包/秒 消费者(主线程槽):1 万包/秒

中间靠无界队列缓冲,结果就是队列无限增长。

正确做法:要么降低投递频率,要么用有界队列 + 丢弃策略。

方案 :批量合并(最常用)

累积 N 个包或等 M 毫秒,一次性 emit:

void UdpFuc::slot_start() { char pBuf[2048]; QByteArray batch; // 累积缓冲 QElapsedTimer timer; timer.start(); const int FLUSH_INTERVAL_MS = 20; // 20ms 刷一次(50 次/秒) const int FLUSH_SIZE = 64 * 1024; // 或累积到 64KB while (!isInterruptionRequested()) { struct sockaddr_in cliAddr; socklen_t addrLen = sizeof(cliAddr); ssize_t ret = recvfrom(m_recvSocket, pBuf, sizeof(pBuf), 0, (struct sockaddr*)&cliAddr, &addrLen); if (ret > 0) { // 追加到批量缓冲,前面加长度前缀方便切分 quint32 len = static_cast<quint32>(ret); batch.append(reinterpret_cast<const char*>(&len), sizeof(len)); batch.append(pBuf, ret); } else if (ret < 0) { if (errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR) ; // 超时,继续检查是否该 flush else break; } // 达到条件就 flush if (batch.size() >= FLUSH_SIZE || (timer.elapsed() >= FLUSH_INTERVAL_MS && !batch.isEmpty())) { emit sign_batch(batch); batch.clear(); timer.restart(); } } // 退出前 flush 剩余 if (!batch.isEmpty()) emit sign_batch(batch); }

效果:

  • 从 10 万次/秒 emit →50 次/秒

  • 事件队列不再堆积

  • 主线程一次处理一批,效率高

代价:引入最多 20ms 延迟(对大多数应用可接受)。


判断标准:如果emit频率 × 主线程单次处理时间 > 1 秒,就会堆积。

10 万次/秒 × 主线程每次 20 微秒 = 2 秒/秒 ← 超过 100%,必然堆积

如何检测是否出问题

打印队列长度

qDebug() << "事件队列长度:" << QCoreApplication::hasPendingEvents(); // 或更精确地测量

问题本质

每包 emit = 每包一次跨线程事件投递 + QByteArray 拷贝。当包速率超过主线程处理速率时,事件队列无限增长,导致内存暴涨、界面卡死、最终丢包。

一句话

emit不是免费的——它是跨线程事件投递。高频下必须批量,否则事件队列会成为内存黑洞。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询