尧图精选

远程桌面工具开发长期常驻的资源回收:线程句柄与内存实测

🕒 发布时间:2026/10/1 8:10:10 📁 来源:尧图网络
05-长期常驻的资源回收线程句柄与内存实测前面四篇把「画面怎么来、线程怎么排、剪贴板怎么同步」都拆完了。最后这篇聊一个看上去不起眼、但决定工具能不能「放心天天开着」的问题资源回收。远程桌面不是跑一下就关的程序——它要开机自启、常驻一整天。一旦哪里的线程、句柄、内存没回收干净跑几小时就会慢慢把机器拖垮。这种 bug短测试永远发现不了只有长期实测才能抓出来。一、为什么必须做长期运行测试先想清楚这个工具的用法被控端公司电脑上的 Agent设计成开机自启、常驻后台你人走了它还在跑第二天来接着跑。这意味着它的生命周期不是「连一下、用一会、关掉」而是「连上、用、断开、重连、再连、再断……循环一整天」。而每一次会话的建立与拆除都会新建一批资源采集线程capture 线程负责抓屏编码剪贴板监听线程clipboard 线程轮询本机剪贴板急停热键线程注册系统热键的后台线程一大堆asyncio 协程发送、接收、watchdog、保活若干socket连中继的 WebSocket 连接。如果其中任意一个在会话结束时没被干净地回收——线程没join掉、事件循环没close、socket 没close——下一次会话又会新建一批。几轮下来线程数缓缓上涨、系统句柄handle被耗尽、内存被悄悄吃掉。等到你下午发现「电脑越来越卡」往往已经是一堆僵尸线程在后台堆积了。这类问题在「连一下就关」的短测试里完全看不出来。你跑一次连接、断开资源变化微乎其微谁都发现不了泄漏。只有把「建 → 用 → 拆」这条路径反复跑很多遍泄漏才会从噪声里浮出来。所以项目源码专门把「长期运行 / 反复重连的资源泄漏」做成了正式测试而不是靠手感。二、测试做法反复强制断开重连 5 轮测试的核心思路是「在真实使用压力下反复折腾」启动被控端建立一次完整会话连中继、配对、开始推流强制断开这次会话不等自然结束模拟网络抖动 / 手动断开等待它按退避逻辑自动重连建立新会话再次强制断开……如此反复5 轮把「建 → 用 → 拆」这条路径累计跑很多遍。为什么要反复断开重连因为真正的泄漏往往发生在「拆」这一步——如果某次拆除漏了join一个线程、漏了close一个 socket每轮都会多留一点垃圾。5 轮下来垃圾的量级足以被测量出来。而采样是最讲究的地方测试在两个时间点各采一次样并对比会话运行中挑某一轮连接稳定、正在推流的时候记录当前线程数、句柄数、内存占用拆除之后所有会话都结束后再记录一次线程数、句柄数、内存占用。只有把这两个时刻摆在一起看才能回答「资源到底回没回收干净」。三、实测数据表项目源码记录的长期运行实测数据如下单位线程 / 句柄 / 内存基线运行中结束后第 1 轮1 / 172 / 43.1 MB4 / 189 / 53.3 MB1 / 176 / 50.3 MB第 5 轮—4 / 189 / 55.2 MB1 / 176 / 50.6 MB三列含义基线程序启动、还没建立任何会话时的初始状态运行中某一轮会话活跃时的峰值状态结束后全部会话拆除、程序仍在运行但已无活动连接时的状态。每一格是「线程数 / 系统句柄数 / 内存占用 MB」。四、逐指标解读线程完全回收、句柄仅残留、内存持平把数据拆开看结论很清楚。线程4 → 1完全回收。运行中线程数是 4基线 1加上采集、剪贴板、急停热键这三个常驻线程结束后回落到 1只剩主线程/事件循环。关键是对比第 1 轮和第 5 轮——运行中都是 4结束后都是 1没有因为多跑了 4 轮就涨到 5、6、7。这说明每一次拆除都把三个工作线程干净地join掉了没有僵尸线程堆积。这正是反复重连测试想验证的核心。句柄残留 4 个且不随轮次增长。基线 172运行中 189多 17 个是这轮会话用到的 socket、事件等结束后 176。注意「结束后」比「基线」多 4 个——这是某些资源比如事件循环自身、日志文件句柄在首次启动后就常驻、不会再释放的部分。重点是第 1 轮的结束后是 176第 5 轮的结束后还是 176。也就是说多跑 4 轮、多建多拆 4 次会话句柄数没有增长。那 4 个残留是固定开销不是泄漏。如果真有泄漏这个数字会一路爬到 180、190、200……内存持平。基线 43.1 MB运行中 53~55 MB多出来的就是画布、编码缓冲、网络缓冲结束后回到 50.3~50.6 MB。第 1 轮和第 5 轮结束后几乎一致差 0.3 MB属测量噪声说明没有内存随轮次累积。画布是每轮重新分配的旧画布能被垃圾回收正确回收没有「越跑越胖」。合起来一句话线程完全回收4 → 1句柄仅残留 4 个且不随轮次增长内存持平。可以放心常驻。五、拆除时到底回收了什么源码视角结论漂亮但它是怎么做到的光喊「记得回收」没用得看拆除这一步具体干了啥。项目源码里每一次会话在finally块里按固定顺序做四件事取消并等待三个协程发送、接收、watchdog 三个任务先cancel()再await gather(..., return_exceptionsTrue)等它们真正退出。asyncio 的协程不是「取消就立刻没」必须等它们跑到退出点否则会变成「永远挂起」的僵尸任务慢慢吃内存。producer.join(timeout3.0)采集线程是daemonTrue的守护线程但守护线程只是「主进程退出时不阻塞退出」并不会自动帮你清理资源。所以显式join等它真正跑完当前帧、退出循环。设 3 秒超时是为了防止极端情况下线程卡死把拆除也卡住——最多等 3 秒就放手。_teardown_injection()与_teardown_clipboard()前者调release_all()松键、再hotkey.stop()注销系统热键并join热键线程后者clipboard.stop()设停止标志并join剪贴板监听线程。这两步把「急停热键线程」和「剪贴板监听线程」都干净收尾。await sess.close()关闭 WebSocket、释放 socket 与底层 TLS 资源并把事件循环该清的缓冲清掉。正是这四步环环相扣才换来「运行中 4 个线程 → 结束后 1 个」。任何一步漏掉——比如忘了join采集线程、忘了stop剪贴板线程——对应线程就会在后台滞留5 轮下来数字立刻现形。长期运行测试的价值就是用重复压力把「哪一步没收干净」逼出来。六、如果回收不干净会发生什么把「正确做法」反过来看能更清楚它的重要性。假设某处线程没join采集线程僵尸化每重连一次多一个几天后线程数爬到几十上百。Windows 下单个进程线程数有上限撞到后程序可能直接起不了新线程、推流彻底罢工。句柄handle耗尽socket、事件对象、剪贴板句柄每轮只开不关句柄数是稀缺资源进程默认上限通常几千。耗尽后任何「打开文件 / 建连接 / 建事件」都会失败表现就是「莫名其妙开始各种报错」。内存缓慢上涨画布缓冲、编码中间对象如果没被引用计数正确释放会随轮次累积。这种上涨极慢一天可能只多几十 MB但常驻一周就可能吃掉上 GB机器越来越卡。这三类问题共同点是短测试看不到只有常驻 反复重连才暴露。所以项目源码把资源回收当成「一等公民」来测而不是交付前临时看一眼。七、长期运行测试的判定标准与 7/7 通过怎么才算「通过」这个长期运行测试项目源码定的判定逻辑很朴素但每条都卡死运行中的线程数必须上来过先确认活跃会话时线程确实爬到 4证明线程真的被创建测试不是空跑。这条就是后面「采样陷阱」要保的底。结束后必须回到 1所有会话拆除后线程数回落到基线附近主线程 常驻事件循环不允许残留工作线程。句柄数结尾不高于开头 固定残留量允许有首次启动的固定开销那 4 个但不允许随轮次单调上升。内存结尾持平结束后内存与基线之差在小范围内波动不随轮次累积。把这几条合起来构成了「建 → 用 → 拆」循环的资源守恒证明。在项目源码的整体测试矩阵里这一类「长期运行 / 反复重连的资源泄漏」测试最终结果是7/7 全部通过——也就是说反复重连、反复建拆这趟压力下来线程、句柄、内存三项指标都守住了。如果你也想在自己机器上验证不需要任何特殊工具把程序开机自启跑一上午中间手动断网重连几次然后用系统自带的任务管理器看进程那一项的「线程数」「句柄数」「内存」三列。健康的表现是——重连时这三个数短暂跳一下稳定后又落回接近初始的值如果某一项随着重连次数一路只增不减那就是哪里没收干净该去查对应线程的join和 socket 的close了。八、一个容易忽略的测试陷阱只在拆除后采样永远是「1 个线程」这部分是整篇最值得记的教训项目源码的实测记录里也坦承「这个测试自己也被这个坑绊过一次」。试想一种偷懒的采样方式你只在「所有会话都结束后」采一次样然后断言「看只有 1 个线程没泄漏」问题是——即使被测代码根本没创建过任何线程拆除后采样也会是「1 个线程」。因为你测的是「程序空闲时」的状态而空闲时本来就只有主线程。你把测试写成这样哪怕被控端从头到尾疯狂泄漏线程你采到的永远是那 1 个空闲线程测试永远「通过」。这样的测试是自我安慰毫无意义。正确的做法必须是同时采样「运行中」状态证明「在干活的时候确实有 4 个线程、有 189 个句柄、有 53 MB 内存」——也就是先证明「测试真的触发了线程/资源的创建」再去看「拆除后它们有没有回到 1/176/50」。只有这两组数字摆在一起对比测试才有效。项目源码的测试正是这么做的先确认运行中爬到了 4/189再确认结束后回到 1/176落差即为「回收干净」的证据。这个陷阱的普适性很强值得任何写「资源泄漏测试」的人引以为戒衡量「有没有泄漏」前提是先证明「被测对象确实创建过资源」。九、结论可以放心常驻把前面所有事实归总被控端在一次完整会话里会创建采集线程、剪贴板监听线程、急停热键线程和若干协程与 socket而经过 5 轮「强制断开—重连」的压力实测线程能从 4 干净回落到 1、句柄残留固定不增长、内存持平不累积。这证明每一处线程的join、事件循环的close、socket 的close、以及画布缓冲的释放都做对了。再补一句常被问到的为什么是 5 轮而不是 50 轮因为泄漏是「线性累积」的——如果每轮漏一个线程5 轮就多 4 个信号已经足够明显真要更严可以加轮次但 5 轮已经能把「回收是否干净」这个核心问题坐实且实测结论稳定。对于「开机自启常驻一整天」的部署形态这个数据足以让人放心。到这里控制端与线程模型这个系列就收尾了。从 Qt 与 asyncio 的双线程分工、画面还原里的「必须复制」、采集线程与 Outbox 背压、剪贴板回环防护到长期常驻的资源回收实测一条贯穿始终的 engineering 主线是远程桌面不是跑一下的 demo而是要在别人电脑上常驻一整天的生产工具所以每一处「不能悄死、不能泄漏、不能撕裂、不能回环」都不是过度设计而是长期运行逼出来的硬要求。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →