该函数运行在子线程中。
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);这一行做了:
malloc分配ret字节堆内存memcpy从pBuf拷贝ret字节构造
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不是免费的——它是跨线程事件投递。高频下必须批量,否则事件队列会成为内存黑洞。