Qt 实时数据可视化工程实践:环形缓冲区实践

news2026/3/31 3:58:51
目录前言一、架构设计1.1 分层架构图1.2 数据写入流1.3 数据刷新流 (定时器驱动 → 视图更新)1.4 核心设计思想二、核心实现详解2.1 RingBuffer环形缓冲区实现2.1.1 append函数线程安全写入函数主体实现第一行QWriteLocker locker(bufferLock)第2-3行数组直接索引写入第4行环形回绕核心逻辑第5行有效数据量管理内存状态演变图解2.1.2 copyTo函数函数主体实现第一行QReadLocker locker(bufferLock) -读写锁QReadLocker第二、三行resize预分配内存四 - 九行循环计算索引写入示例演算承接上面的 F、B、C、D、E情况一缓冲区未满逻辑顺序旧→新A(0)、B(1)、C(2)、[ ]、[ ]情况二缓冲区刚好满情况三缓冲区满且回绕2.1.3 accessData函数函数主体实现1. 计算最旧数据的位置逻辑起始点2. 计算两段连续内存长度函数作用避免堆分配不创建临时 QVector减少内存碎片CPU缓存友好数据直接在原内存位置被消费提高缓存命中率支持流式处理可在回调中直接写入文件、网络发送无需中转缓冲区实现缺陷总结2.1.4 setCapacity函数函数主体实现前言在工业监控、传感器数据采集、金融行情等场景中需要处理高频率100Hz的实时数据流同时保持 UI 的流畅性60fps。传统的追加数据-全量重绘模式会导致内存持续增长和界面卡顿。本文介绍一种基于环形缓冲区Ring Buffer的生产级解决方案。效果演示示例图中两条曲线是构造数据添加上去的一条是正弦波一条是方波正弦波设置了数据容量10000点方波设置了数据容量30000点数据刷新速率是30fps添加频率是1kHz这是没有启用动态时间轴的演示下面是使用动态时间轴的演示源码地址在文末。一、架构设计1.1 分层架构图1.2 数据写入流1.3 数据刷新流 (定时器驱动 → 视图更新)1.4 核心设计思想数据与视图分离PlotCurveDataModel只管理数据PlotCurveController负责同步到QCustomPlotPlotRefresher负责固定时长刷新视图固定内存占用RingBuffer预分配内存避免频繁的堆分配线程安全采用QReadWriteLock实现多读单写支持后台采集线程直接写入零拷贝优化提供accessData回调接口避免不必要的数据拷贝。二、核心实现详解2.1 RingBuffer环形缓冲区实现#include QVector #include QReadWriteLock class RingBuffer { public: explicit RingBuffer(int capacity 10000); void append(double key, double value); // 写 void copyTo(QVectordouble xs, QVectordouble ys) const; // 读 void accessData(std::functionvoid(const double*, const double*, int) callback) const; int size() const; int capacity() const; bool isFull() const; void setCapacity(int capacity); private: int bufferCapacity 10000; int bufferHead 0; // 写入位置 int bufferDataCount 0; // 当前元素数 QVectordouble _key; QVectordouble _value; mutable QReadWriteLock bufferLock; };//环形缓冲区实现 RingBuffer::RingBuffer(int capacity) : bufferCapacity(std::max(capacity, 10000)) { _key.resize(bufferCapacity); _value.resize(bufferCapacity); } //追加一点数据 void RingBuffer::append(double key, double value) { QWriteLocker locker(bufferLock); _key[bufferHead] key; _value[bufferHead] value; bufferHead (bufferHead 1) % bufferCapacity; if (bufferDataCount bufferCapacity) bufferDataCount; } //返回拷贝视图 void RingBuffer::copyTo(QVectordouble xs, QVectordouble ys) const { QReadLocker locker(bufferLock); xs.resize(bufferDataCount); ys.resize(bufferDataCount); for (int i 0; i bufferDataCount; i) { int idx (bufferHead - bufferDataCount i bufferCapacity) % bufferCapacity; xs[i] _key[idx]; ys[i] _value[idx]; } } //零拷贝数据访问 通过回调实现 void RingBuffer::accessData(std::functionvoid (const double *, const double *, int) callback) const { QReadLocker locker(bufferLock); // 处理环形缓冲区的非连续内存可能分两段 int firstSegment std::min(bufferDataCount, bufferCapacity - (bufferHead - bufferDataCount bufferCapacity) % bufferCapacity); int secondSegment bufferDataCount - firstSegment; int startIdx (bufferHead - bufferDataCount bufferCapacity) % bufferCapacity; if (secondSegment 0) { // 数据连续直接回调 callback(_key.constData() startIdx, _value.constData() startIdx, bufferDataCount); } else { // 数据分两段回调两次或合并到临时缓冲区 callback(_key.constData() startIdx, _value.constData() startIdx, firstSegment); callback(_key.constData(), _value.constData(), secondSegment); } } //获取当前数据量 int RingBuffer::size() const { QReadLocker locker(bufferLock); return bufferDataCount; } //获取当前缓冲区大小 int RingBuffer::capacity() const { QReadLocker locker(bufferLock); return bufferCapacity; } //判满 bool RingBuffer::isFull() const { QReadLocker locker(bufferLock); return (bufferDataCount bufferCapacity); } //重置容量 void RingBuffer::setCapacity(int capacity) { if (capacity 0 || capacity bufferCapacity) return; // 如果扩容且容量增幅不大考虑原地 reallocQVector 自动处理 if (capacity bufferCapacity) { QWriteLocker w(bufferLock); _key.resize(capacity); _value.resize(capacity); bufferCapacity capacity; return; } // 在单次写锁内完成所有操作 QWriteLocker w(bufferLock); // 如果新容量小于当前数据量只保留最新的 capacity 条 int newDataCount qMin(bufferDataCount, capacity); QVectordouble newKey(capacity); QVectordouble newValue(capacity); // 复制最新的 newDataCount 条数据 for (int i 0; i newDataCount; i) { int srcIdx (bufferHead - newDataCount i bufferCapacity) % bufferCapacity; newKey[i] _key[srcIdx]; newValue[i] _value[srcIdx]; } _key std::move(newKey); _value std::move(newValue); bufferHead newDataCount; bufferDataCount newDataCount; bufferCapacity capacity; }下面对关键实现以及细节进行详细探讨2.1.1 append函数函数主体实现void RingBuffer::append(double key, double value) { QWriteLocker locker(bufferLock); _key[bufferHead] key; _value[bufferHead] value; bufferHead (bufferHead 1) % bufferCapacity; if (bufferDataCount bufferCapacity) bufferDataCount; }第一行QWriteLocker locker(bufferLock)QWriteLocker locker(bufferLock);可以看到函数进入后使用QWriteLocker locker(bufferLock);实现线程安全屏障其作用有RAII机制构造时加写锁析构时函数退出自动解锁异常安全即使抛出异常也能解锁互斥策略阻止其他读线程QReadLocker和写线程访问保证原子性写入性能代价无竞争时约20-50ns高竞争时可能阻塞但比QMutex更适合多读场景第2-3行数组直接索引写入_key[bufferHead] key; _value[bufferHead] value;O(1)时间复杂度直接内存寻址无查找开销预分配优势_key和_value在构造函数中已通过resize预分配无堆分配这是与std::vector::push_back的本质区别缓存友好两次连续内存写入如果key和value数组相邻CPU缓存命中率高第4行环形回绕核心逻辑bufferHead (bufferHead 1) % bufferCapacity;模运算当bufferHead到达数组末尾capacity-1时(capacity-1 1) % capacity 0回到开头FIFO策略新数据永远覆盖最旧的数据bufferHead指向的位置无分支预测模运算比分支判断if (head capacity) head 0;更适合CPU流水线注意这里bufferHead始终指向下一个写入的位置也就是当回绕时其始终指向最老数据的位置第5行有效数据量管理if (bufferDataCount bufferCapacity) bufferDataCount;双状态区分未满状态bufferDataCount capacity缓冲区在填充阶段count表示实际数据量已满状态bufferDataCount capacity缓冲区满bufferDataCount保持恒定新数据覆盖旧数据读取边界告诉读线程应该读取多少个元素从bufferHead-bufferDataCount到bufferHead内存状态演变图解假设capacity 5写入序列[A, B, C, D, E, F, G]环形缓冲区内存示意表步骤写入bufferHeadbufferDataCount内存状态_key数组说明初始-00[_, _, _, _, _]空缓冲区1A11[A, _, _, _, _]正常填充2B22[A, B, _, _, _]正常填充3C33[A, B, C, _, _]正常填充4D44[A, B, C, D, _]正常填充5E05[A, B, C, D, E]首次回绕满状态6F15[F, B, C, D, E]覆盖A保留B-E7G25[F, G, C, D, E]覆盖B保留C-G关键当写入第6个元素F时bufferHead回到0覆盖了最旧的A但bufferDataCount保持5满容量。这样做的好处是内存大小占用恒定不会因为长时间运行导致内存爆炸卡死用过QCustomplot的读者都知道长时间绘图若没有限制数据点数量会导致内存不断膨胀卡死其中另外一种解决办法是删除最旧的数据点在QCustomplot库中手动添加一个removeDataBefore方法具体可见如下这篇博客http://【Qcustomplot内存超出问题 - CSDN App】https://blog.csdn.net/weixin_44802704/article/details/132401999?sharetypeblogshareId132401999sharereferAPPsharesourceh6030sharefromlinkhttp://【Qcustomplot内存超出问题 - CSDN App】https://blog.csdn.net/weixin_44802704/article/details/132401999?sharetypeblogshareId132401999sharereferAPPsharesourceh6030sharefromlink而本文的实现更适用于固定窗口大小的实时数据流但在高频场景1kHz下建议配合批量写入或双缓冲策略将锁竞争降到最小。2.1.2 copyTo函数函数主体实现void RingBuffer::copyTo(QVectordouble xs, QVectordouble ys) const { QReadLocker locker(bufferLock); xs.resize(bufferDataCount); ys.resize(bufferDataCount); for (int i 0; i bufferDataCount; i) { int idx (bufferHead - bufferDataCount i bufferCapacity) % bufferCapacity; xs[i] _key[idx]; ys[i] _value[idx]; } }这是环形缓冲区Circular Buffer/Ring Buffer最核心的数据导出函数。其功能是将环形缓冲区中的数据按时间顺序从旧到新拷贝到两个QVector中供绘图使用。第一行QReadLocker locker(bufferLock) -读写锁QReadLockerQReadLocker locker(bufferLock);允许多个读线程并发执行copyTo阻塞写线程append中的QWriteLocker函数中bufferLock声明为mutable符合逻辑常量性第二、三行resize预分配内存xs.resize(bufferDataCount); ys.resize(bufferDataCount);使用resize而非reserve确保operator[]安全且最终size()正确如果外部传入的xs/ys已有数据会被截断或扩展四 - 九行循环计算索引写入for (int i 0; i bufferDataCount; i) { int idx (bufferHead - bufferDataCount i bufferCapacity) % bufferCapacity; xs[i] _key[idx]; ys[i] _value[idx]; }模运算优化现代编译器会将% bufferCapacity优化为位运算当 capacity 为 2^n 时 bufferCapacity是关键技巧处理bufferHead bufferDataCount时的负数情况这里其实现的是物理上数据是循环存储的逻辑上线性输出按数据时间对照环形缓冲区内存示意表逐步推导一下索引计算公式int idx (bufferHead - bufferDataCount i bufferCapacity) % bufferCapacity部分含义bufferHead - bufferDataCount定位到最老数据的起始位置 i偏移到当前要读取的第 i 个元素 bufferCapacity防止负数确保模运算前为正% bufferCapacity处理回绕wrap-around示例演算承接上面的 F、B、C、D、E情况一缓冲区未满逻辑顺序旧→新A(0)、B(1)、C(2)、[ ]、[ ]数组[A, B, C, -, -]当前状态bufferHead 3,bufferDataCount 3,bufferCapacity 5计算i 0取A(3 - 3 0 5) % 5 (5) % 5 0同理得i 1取B,i2取C这里只能取到2因为for循环的条件是i bufferDataCount,即i 3情况二缓冲区刚好满逻辑顺序旧→新A(0)、B(1)、C(2)、D(3)、E(4)数组[A, B, C, D, E]当前状态bufferHead 0,bufferDataCount 5,bufferCapacity 5计算i 0取A(0 - 5 0 5) % 5 (0) % 5 0依次循环i 5取0 - 4即对应[A, B, C, D, E]逐个输出到xsys容器其就是已经按时间顺序排好了。情况三缓冲区满且回绕逻辑顺序旧→新C(2)、D(3)、E(4)、F(0)、G(1)数组[F, G, C, D, E]当前状态bufferHead 2,bufferDataCount 5,bufferCapacity 5计算i 0取C(2 - 5 0 5) % 5 (2) % 5 2计算i 2取E(2 - 5 2 5) % 5 (4) % 5 4当 i 2时实现索引回绕计算如 i 3此时 idx 0i 4此时 idx 1此时按照循环0 - 4idx索引输出为23401即对应最老数据 - 最新数据2.1.3 accessData函数函数主体实现void RingBuffer::accessData(std::functionvoid (const double *, const double *, int) callback) const { QReadLocker locker(bufferLock); // 处理环形缓冲区的非连续内存可能分两段 int firstSegment std::min(bufferDataCount, bufferCapacity - (bufferHead - bufferDataCount bufferCapacity) % bufferCapacity); int secondSegment bufferDataCount - firstSegment; int startIdx (bufferHead - bufferDataCount bufferCapacity) % bufferCapacity; if (secondSegment 0) { // 数据连续直接回调 callback(_key.constData() startIdx, _value.constData() startIdx, bufferDataCount); } else { // 数据分两段回调两次或合并到临时缓冲区 callback(_key.constData() startIdx, _value.constData() startIdx, firstSegment); callback(_key.constData(), _value.constData(), secondSegment); } }这是一个零拷贝Zero-Copy数据访问接口的实现核心目的是在不复制数据的前提下安全地读取环形缓冲区中可能物理不连续的内存数据通过Callback回调在一次锁内访问数据。其两次回调原因是环形缓冲区在逻辑上按时间是需要连续的[0→1→2→...→N]但物理内存可能断裂数据内容: d5 d6 d7 d8 d9 d10 d1 d2 d3 d4 物理索引: [0] [1] [2] [3] [4] [5] [6] [7] [8] [9] ↑head ↑start1. 计算最旧数据的位置逻辑起始点int startIdx (bufferHead - bufferDataCount bufferCapacity) % bufferCapacity;bufferHead下一个写入位置当前为空或即将被覆盖bufferHead - bufferDataCount回退 N 个元素到最旧数据 bufferCapacity防止负数保证取模正确% bufferCapacity回绕到合法索引2. 计算两段连续内存长度// 计算第一段连续内存的长度 int firstSegment std::min( bufferDataCount, // 总数据量上限 bufferCapacity - startIdx // 从startIdx到缓冲区末尾的空间 ); // 第二段长度回绕部分 int secondSegment bufferDataCount - firstSegment;如果startIdx7, capacity10则剩余空间3索引7,8,9如果数据量6 3则第一段只能取3个剩下3个在索引0,1,2第二段函数作用避免堆分配不创建临时QVector减少内存碎片CPU缓存友好数据直接在原内存位置被消费提高缓存命中率支持流式处理可在回调中直接写入文件、网络发送无需中转缓冲区实现缺陷如果callback中执行了任何耗时操作绘图、文件IO、网络请求、甚至QThread::msleep整个写入线程会被阻塞所以回调中应该只做最轻量的操作如指针转发、快速拷贝到线程本地存储改进可考虑双缓冲Double Buffering维护两个缓冲区交替使用写入时写到后台缓冲区完成后原子交换指针。读取总是看到物理连续的前台缓冲区。其读取时无需锁原子指针交换物理连续真正Zero-Copy但是其缺点是内存占用翻倍写入时有memmove开销滑动窗口时。或者使用虚拟内存魔术Linux高级技巧利用mmap创建环形虚拟地址映射让同一物理页映射到虚拟地址空间的两个位置实现物理断裂但虚拟连续。优点是单次回调Zero-Copy无内存拷贝缺点平台依赖Linux onlyWindows实现极难调试困难页对齐浪费内存。总结accessData是一个面向高性能场景的接口其解决了环形缓冲区的物理不连续性问题通过分段回调实现逻辑连续性的抽象。但需要保证以下使用铁律回调中严禁耗时操作IO、复杂计算、UI更新回调中严禁再次调用 RingBuffer 的任何方法死锁风险如果数据需要跨线程使用必须拷贝copyTo更安全由于下游是QCustomPlot其setData函数要求QVectordouble连续内存。两段式回调对QCustomPlot并不友好所以此处这个函数并没有使用到。在追求极致性能情况下应放弃setData改用QCPGraph::data()获取内部指针直接写入这个感兴趣的读者可以自行研究。2.1.4 setCapacity函数函数主体实现void RingBuffer::setCapacity(int capacity) { if (capacity 0 || capacity bufferCapacity) return; // 如果扩容且容量增幅不大考虑原地 reallocQVector 自动处理 if (capacity bufferCapacity) { QWriteLocker w(bufferLock); _key.resize(capacity); _value.resize(capacity); bufferCapacity capacity; return; } // 在单次写锁内完成所有操作 QWriteLocker w(bufferLock); // 如果新容量小于当前数据量只保留最新的 capacity 条 int newDataCount qMin(bufferDataCount, capacity); QVectordouble newKey(capacity); QVectordouble newValue(capacity); // 复制最新的 newDataCount 条数据 for (int i 0; i newDataCount; i) { int srcIdx (bufferHead - newDataCount i bufferCapacity) % bufferCapacity; newKey[i] _key[srcIdx]; newValue[i] _value[srcIdx]; } _key std::move(newKey); _value std::move(newValue); bufferHead newDataCount; bufferDataCount newDataCount; bufferCapacity capacity; }这是一个线程安全的动态容量调整实现针对环形缓冲区的物理非连续性特性做了特殊处理。该函数实现了缩容整理、扩容原地的策略确保在任何情况下数据逻辑顺序都不丢失。1. 函数整体策略场景策略数据整理时间复杂度扩容(capacity old)原地 realloc不整理依赖QVector::resizeO(old) 或 O(1)缩容(capacity old)重建复制强制整理为物理连续丢弃旧数据O(min(old, new))2. 参数检查if (capacity 0 || capacity bufferCapacity) return;拒绝无效容量0如果容量没变化直接返回3. 扩容分支if (capacity bufferCapacity) { QWriteLocker w(bufferLock); _key.resize(capacity); _value.resize(capacity); bufferCapacity capacity; return; }为什么这么简单当bufferHead bufferDataCount数据已回绕时数据物理分布可能是索引: 0 1 2 ... head ... capacity-1 数据: d5 d6 -- d1 d2 d3 d4 ↑新数据 ↑旧数据将被覆盖QVector::resize()在扩容时如果原地有足够空间直接扩展尾部realloc可能移动整内存块但保持已有数据相对位置无论是否移动内存已有元素的顺序和值不变因此扩容后回绕状态依然保持head 和 count 无需修改新空间自然成为尾部缓冲区。风险如果频繁在满回绕状态下小幅扩容每次resize都可能触发全量memcpy将整个缓冲区复制到新内存块。4. 缩容分支// 只保留最新的 capacity 条数据 int newDataCount qMin(bufferDataCount, capacity); QVectordouble newKey(capacity); QVectordouble newValue(capacity); // 复制最新的 newDataCount 条数据 for (int i 0; i newDataCount; i) { int srcIdx (bufferHead - newDataCount i bufferCapacity) % bufferBuffer; newKey[i] _key[srcIdx]; newValue[i] _value[srcIdx]; }索引计算逻辑假设bufferHead3, bufferDataCount10, bufferCapacity10数据已回绕最老数据索引(3 - 10 10) % 10 3d4物理内存布局索引: 0 1 2 3 4 5 6 7 8 9 数据: d11 d12 d13 d4 d5 d6 d7 d8 d9 d10 ↑head3 ↑start5现在要缩容到capacity 6保留最新的 6 条d8, d9, d10, d11, d12, d13newDataCount min(10, 6) 6循环 i 0 ~ 5i 0:srcIdx (3 - 6 0 10) % 10 7→ d8最老i 1:srcIdx (3 - 6 1 10) % 10 8→ d9i 2:srcIdx (3 - 6 2 10) % 10 9→ d10i 3:srcIdx (3 - 6 3 10) % 10 0→ d11i 4:srcIdx (3 - 6 4 10) % 10 1→ d12i 4:srcIdx (3 - 6 5 10) % 10 2→ d13最新新缓冲区内存布局newKey: [d8, d9, d10, d11, d12, d13] 索引: 0 1 2 3 4 5结果数据从物理断裂回绕整理为物理连续且bufferHead newDataCount 6指向末尾准备下次写入。5. 内存优化_key std::move(newKey); _value std::move(newValue);为什么用std::move避免二次拷贝如果不使用moveQVector的赋值操作会触发深拷贝复制所有 double 元素指针转移std::move后newKey的内部指针直接转移给_key原newKey变为空旧内存释放原_key旧的环形缓冲区内存在赋值语句结束后自动析构释放等价于_key.swap(newKey); // Qt 容器也支持 swap效果类似6. 状态重置bufferHead newDataCount; bufferDataCount newDataCount; bufferCapacity capacity;为什么bufferHead newDataCount缩容后数据被整理为从索引 0 开始连续存储到newDataCount - 1此时bufferHead指向newDataCount即最后一个有效数据之后的位置如果newDataCount capacity缩容后刚好填满head指向缓冲区末尾下次写入会自然回绕到 0bufferDataCount正确反映当前数据量这保持了环形缓冲器的不变量bufferDataCount表示[head-count, head)区间内的有效数据量。总结该函数可实现动态容量调整适用于实时波形显示中动态调整 时间窗长度如从显示 10秒改为显示 5秒数据能平滑处理而不丢失最新采样点。三、源码地址Gitee码云https://gitee.com/mishm/qcustomplot-circular-bufferGithubhttps://github.com/H0138mw/-QCustomplot_Circle_Buffer

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2467195.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

SpringBoot-17-MyBatis动态SQL标签之常用标签

文章目录 1 代码1.1 实体User.java1.2 接口UserMapper.java1.3 映射UserMapper.xml1.3.1 标签if1.3.2 标签if和where1.3.3 标签choose和when和otherwise1.4 UserController.java2 常用动态SQL标签2.1 标签set2.1.1 UserMapper.java2.1.2 UserMapper.xml2.1.3 UserController.ja…

wordpress后台更新后 前端没变化的解决方法

使用siteground主机的wordpress网站,会出现更新了网站内容和修改了php模板文件、js文件、css文件、图片文件后,网站没有变化的情况。 不熟悉siteground主机的新手,遇到这个问题,就很抓狂,明明是哪都没操作错误&#x…

网络编程(Modbus进阶)

思维导图 Modbus RTU(先学一点理论) 概念 Modbus RTU 是工业自动化领域 最广泛应用的串行通信协议,由 Modicon 公司(现施耐德电气)于 1979 年推出。它以 高效率、强健性、易实现的特点成为工业控制系统的通信标准。 包…

UE5 学习系列(二)用户操作界面及介绍

这篇博客是 UE5 学习系列博客的第二篇,在第一篇的基础上展开这篇内容。博客参考的 B 站视频资料和第一篇的链接如下: 【Note】:如果你已经完成安装等操作,可以只执行第一篇博客中 2. 新建一个空白游戏项目 章节操作,重…

IDEA运行Tomcat出现乱码问题解决汇总

最近正值期末周,有很多同学在写期末Java web作业时,运行tomcat出现乱码问题,经过多次解决与研究,我做了如下整理: 原因: IDEA本身编码与tomcat的编码与Windows编码不同导致,Windows 系统控制台…

利用最小二乘法找圆心和半径

#include <iostream> #include <vector> #include <cmath> #include <Eigen/Dense> // 需安装Eigen库用于矩阵运算 // 定义点结构 struct Point { double x, y; Point(double x_, double y_) : x(x_), y(y_) {} }; // 最小二乘法求圆心和半径 …

使用docker在3台服务器上搭建基于redis 6.x的一主两从三台均是哨兵模式

一、环境及版本说明 如果服务器已经安装了docker,则忽略此步骤,如果没有安装,则可以按照一下方式安装: 1. 在线安装(有互联网环境): 请看我这篇文章 传送阵>> 点我查看 2. 离线安装(内网环境):请看我这篇文章 传送阵>> 点我查看 说明&#xff1a;假设每台服务器已…

XML Group端口详解

在XML数据映射过程中&#xff0c;经常需要对数据进行分组聚合操作。例如&#xff0c;当处理包含多个物料明细的XML文件时&#xff0c;可能需要将相同物料号的明细归为一组&#xff0c;或对相同物料号的数量进行求和计算。传统实现方式通常需要编写脚本代码&#xff0c;增加了开…

LBE-LEX系列工业语音播放器|预警播报器|喇叭蜂鸣器的上位机配置操作说明

LBE-LEX系列工业语音播放器|预警播报器|喇叭蜂鸣器专为工业环境精心打造&#xff0c;完美适配AGV和无人叉车。同时&#xff0c;集成以太网与语音合成技术&#xff0c;为各类高级系统&#xff08;如MES、调度系统、库位管理、立库等&#xff09;提供高效便捷的语音交互体验。 L…

(LeetCode 每日一题) 3442. 奇偶频次间的最大差值 I (哈希、字符串)

题目&#xff1a;3442. 奇偶频次间的最大差值 I 思路 &#xff1a;哈希&#xff0c;时间复杂度0(n)。 用哈希表来记录每个字符串中字符的分布情况&#xff0c;哈希表这里用数组即可实现。 C版本&#xff1a; class Solution { public:int maxDifference(string s) {int a[26]…

【大模型RAG】拍照搜题技术架构速览:三层管道、两级检索、兜底大模型

摘要 拍照搜题系统采用“三层管道&#xff08;多模态 OCR → 语义检索 → 答案渲染&#xff09;、两级检索&#xff08;倒排 BM25 向量 HNSW&#xff09;并以大语言模型兜底”的整体框架&#xff1a; 多模态 OCR 层 将题目图片经过超分、去噪、倾斜校正后&#xff0c;分别用…

【Axure高保真原型】引导弹窗

今天和大家中分享引导弹窗的原型模板&#xff0c;载入页面后&#xff0c;会显示引导弹窗&#xff0c;适用于引导用户使用页面&#xff0c;点击完成后&#xff0c;会显示下一个引导弹窗&#xff0c;直至最后一个引导弹窗完成后进入首页。具体效果可以点击下方视频观看或打开下方…

接口测试中缓存处理策略

在接口测试中&#xff0c;缓存处理策略是一个关键环节&#xff0c;直接影响测试结果的准确性和可靠性。合理的缓存处理策略能够确保测试环境的一致性&#xff0c;避免因缓存数据导致的测试偏差。以下是接口测试中常见的缓存处理策略及其详细说明&#xff1a; 一、缓存处理的核…

龙虎榜——20250610

上证指数放量收阴线&#xff0c;个股多数下跌&#xff0c;盘中受消息影响大幅波动。 深证指数放量收阴线形成顶分型&#xff0c;指数短线有调整的需求&#xff0c;大概需要一两天。 2025年6月10日龙虎榜行业方向分析 1. 金融科技 代表标的&#xff1a;御银股份、雄帝科技 驱动…

观成科技:隐蔽隧道工具Ligolo-ng加密流量分析

1.工具介绍 Ligolo-ng是一款由go编写的高效隧道工具&#xff0c;该工具基于TUN接口实现其功能&#xff0c;利用反向TCP/TLS连接建立一条隐蔽的通信信道&#xff0c;支持使用Let’s Encrypt自动生成证书。Ligolo-ng的通信隐蔽性体现在其支持多种连接方式&#xff0c;适应复杂网…

铭豹扩展坞 USB转网口 突然无法识别解决方法

当 USB 转网口扩展坞在一台笔记本上无法识别,但在其他电脑上正常工作时,问题通常出在笔记本自身或其与扩展坞的兼容性上。以下是系统化的定位思路和排查步骤,帮助你快速找到故障原因: 背景: 一个M-pard(铭豹)扩展坞的网卡突然无法识别了,扩展出来的三个USB接口正常。…

未来机器人的大脑:如何用神经网络模拟器实现更智能的决策?

编辑&#xff1a;陈萍萍的公主一点人工一点智能 未来机器人的大脑&#xff1a;如何用神经网络模拟器实现更智能的决策&#xff1f;RWM通过双自回归机制有效解决了复合误差、部分可观测性和随机动力学等关键挑战&#xff0c;在不依赖领域特定归纳偏见的条件下实现了卓越的预测准…

Linux应用开发之网络套接字编程(实例篇)

服务端与客户端单连接 服务端代码 #include <sys/socket.h> #include <sys/types.h> #include <netinet/in.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <arpa/inet.h> #include <pthread.h> …

华为云AI开发平台ModelArts

华为云ModelArts&#xff1a;重塑AI开发流程的“智能引擎”与“创新加速器”&#xff01; 在人工智能浪潮席卷全球的2025年&#xff0c;企业拥抱AI的意愿空前高涨&#xff0c;但技术门槛高、流程复杂、资源投入巨大的现实&#xff0c;却让许多创新构想止步于实验室。数据科学家…

深度学习在微纳光子学中的应用

深度学习在微纳光子学中的主要应用方向 深度学习与微纳光子学的结合主要集中在以下几个方向&#xff1a; 逆向设计 通过神经网络快速预测微纳结构的光学响应&#xff0c;替代传统耗时的数值模拟方法。例如设计超表面、光子晶体等结构。 特征提取与优化 从复杂的光学数据中自…