热血江湖助手 · 分析资料站← 报告总目录助手主页
分区:15_助手版本迭代说明源文件:日志加线程许可_改动说明.md篇幅:12.8 KB收录:2026-10-19 17:10

日志链路加入线程许可(临界区)· 改动说明


1. 为什么改

症状:挂机一段时间后程序无任何提示直接消失(不是报错退出、不是闪退提示,是进程没了)。

根因:写日志 全工程 681 个调用点、34 个工作线程入口,最终都只做这一句:

日志缓冲 = 日志缓冲 + 日志文本

日志缓冲 是全局文本型变量;界面线程装了 9971 定时器,每 500 毫秒执行一次:

新日志 = 日志缓冲
日志缓冲 = “”

两边都没有加锁。易语言里 A = A + B 是「取旧值 → 申请新块 → 拷贝 → 释放旧块 → 写回」五步非原子操作。两边交错执行时会出现:

两种都会破坏 Windows 堆结构。堆一旦损坏,后续任意一次内存分配都可能直接让进程消失 —— 这解释了「无提示、无日志、进程凭空不见」。


2. 改了什么(4 处 + 1 个全局变量)

2.1 新增全局变量

文件:旧版本更新.代码/全局变量.e.txt(末尾追加)

.全局变量 日志许可证, 许可证, , , 【线程许可】日志互斥锁(Windows 临界区):写日志/写封包日志 与 界面线程 日志定时回调 共用同一把;由 _MainDialogWnd_创建完毕 里 许可证_创建 建立

模块已把它和 许可证_创建 / 许可证_进入 / 许可证_离开 / 许可证_尝试进入 / 许可证_删除 一起 公开,<br>零新增声明即可直接用。

锁会指向垃圾内存——这是易语言里加锁最容易犯的错。

2.2 建锁:必须早于任何会写日志的线程

文件:代码/窗口程序集_MainDialogWnd.form.e.txt · _MainDialogWnd_创建完毕(第 21~23 行)

    默认配置文件路径 = 全局目录 + “\Default.ini”
    ' 【线程许可】先建锁再起线程:日志锁必须早于任何会写日志的线程存在,全进程只创建这一次
    许可证_创建 (日志许可证)
    启动线程 (&自动进入游戏, , 自动进入游戏线程)

位置很关键:自动进入游戏 线程的第二行就调 写日志,下面第 24 行的 9971 定时器也会立刻开始跑。<br>所以建锁必须排在 启动线程 与 创建定时器 之前。全进程只创建一次,不要在用到的地方顺手创建<br>(那样每次都是一把新锁,等于没加)。

2.3 写日志:入锁包住「读取-拼接-写回」

.子程序 写日志, , , 【写法优化】线程安全日志入口:…
    .参数 日志文本, 文本型, , 要记录的日志内容(通常以 #换行符 结尾)

    ' 【线程许可】与界面线程 日志定时回调 争抢同一个全局文本变量:入锁保证“读取-拼接-写回”三步原子,
    ' 否则并发下同一块字符串内存被释放两次,堆被破坏,进程无任何提示直接消失
    许可证_进入 (日志许可证)
    日志缓冲 = 日志缓冲 + 日志文本
    许可证_离开 (日志许可证)

许可证_进入 在拿不到锁时会一直等(不返回),拿到才往下走;许可证_离开 放行一个等待者。<br>681 个调用点一行没动——加锁这件事只发生在被调用的 写日志 内部。

2.4 写封包日志:同一把锁

    ' 【线程许可】封包日志由 HOOK 回调线程写入,与界面线程定时器共用同一把锁
    许可证_进入 (日志许可证)
    封包日志缓冲 = 封包日志缓冲 + 日志文本
    许可证_离开 (日志许可证)

封包日志缓冲 由被 HOOK 的那些线程写入、界面线程取走,是同一类竞态,所以一起收进同一把锁。

2.5 日志定时回调:锁内只摘取,慢操作全部挪到锁外

这是本次最需要动脑的一处。原来的写法是「判断 → 取走 → 清空 → 刷控件 → 落盘」整段连着跑。<br>如果整段加锁,界面线程一次 写到文件 就会把所有工作线程的 写日志 全部堵住 → 整个挂机卡顿。

所以改成:锁内只做三行(取走 + 清空),剩下的控件刷新与落盘放到锁外。

    ' 【写法优化】本子程序由创建定时器(9971)安装在界面线程,每500毫秒触发一次,统一完成“日志显示+日志落盘”
    ' 【线程许可】只把“摘取并清空缓冲”这几行放进锁内;刷控件与落盘是慢操作,必须放到锁外,
    ' 否则所有工作线程的 写日志 都会被界面线程的一次文件写入挡住,表现为整个挂机卡顿
    许可证_进入 (日志许可证)
    新日志 = 日志缓冲
    日志缓冲 = “”
    许可证_离开 (日志许可证)
    .如果真 (新日志 ≠ “”)
        ' 编辑框长度上限:超长只保留最近一半内容,防止长时间挂机内存与刷新开销无限增长
        .如果真 (取文本长度 (挂机信息.内容) > 60000)
            挂机信息.内容 = 取文本右边 (挂机信息.内容, 30000)
        .如果真结束
        挂机信息.加入文本 (新日志) ' 注意:此处必须直写控件(本子程序已在界面线程),不得改成写日志,否则日志会无限回环
        ' 增量落盘:只把本次新增的日志追加进文件,不再整框重写
        日志路径 = 全局目录 + “\log\” + 全局_人物名称 + “.txt”
        写到文件 (日志路径, 读入文件 (日志路径) + 到字节集 (新日志))
    .如果真结束
    许可证_进入 (日志许可证)
    新日志 = 封包日志缓冲
    封包日志缓冲 = “”
    许可证_离开 (日志许可证)
    .如果真 (新日志 ≠ “”)
        .如果真 (取文本长度 (封包拦截框.内容) > 60000)
            封包拦截框.内容 = 取文本右边 (封包拦截框.内容, 30000)
        .如果真结束
        封包拦截框.加入文本 (新日志)
    .如果真结束

要点:

这样改动的变量声明面为零,也就绕开了易语言「.局部变量 声明区不能被注释打断」的坑。

效果完全相同,代价是两次进出锁(纳秒级)。

即便万一重入,Windows 临界区对同一线程是可重入的(RecursionCount 递增),也不会死锁。


3. 验证(七项全绿)

#检查结果
1补丁自身断言(进出成对 4:4、建锁 1 处、写日志 调用点 681 不变、.如果真 块配平、含非 GBK 字符扫描)全部通过
2BOM / 换行主本 BOM=True,CRLF=12799,LF-only=0(与改前一致)
3t2e 回转0 错误 0 警告,18480 条日志全部是「信息」级;SUCC:D:/AI空间/rxjh/_patch/out_lock.e
4项目.etprjt2e 后已用 _patch/项目.etprj.pre12 还原,diff 与备份一致
5e2t 反查(把生成的 .e 再展开)4 组 许可证_进入 / 许可证_离开 全部是真代码(没有被降级成注释)
6分层保真 fidelity.pyL1 代码级 0 处不一致(466 个子程序全对);全局行代码一致 True;L2 注释变化 13 个子程序 / 42 条,全部是历史已知的行尾注释损耗,与本次改动无关
7二进制确认日志许可证 改前 0 次 → 改后 1 次;许可证_创建 1 → 2;4 处加锁点全部落地

改动范围(与 _patch/日志加许可前_20261006_2238/ 对比):只落在 4 个区段,+21 行 / −9 行,<br>没有波及其它任何子程序。


4. ⚠️ 必须知道的残留风险(本轮未处理,等你拍板)

许可证_进入 是阻塞式的:拿不到锁就一直等。这把锁本身没问题,但它和工程里另一个既有做法<br>组合时会出现一个小概率故障:

停止挂机(第 11509 行起)在软停 5 秒后,会对 回补线程 / 回补下晶线程 / 挂机寻路线程 /<br>多坐标寻路线程 / 存卖石头线程 依次执行 强制结束线程(全工程共 25 处)。易语言这个命令<br>对应 Windows 的 TerminateThread —— 它不会释放被结束线程持有的临界区。

于是理论上存在这样一条路径:

某个线程刚好停在 许可证_进入 … 许可证_离开 这中间三个指令里被枪毙
→ 日志许可证 永久停在「已进入」状态
→ 之后任何一次 写日志 都会永远等下去
→ 而 停止挂机 第 11560 行自己就调了一次 写日志,且它跑在界面线程上
→ 界面线程卡死,整个程序假死(进程还在,窗口不动)。

概率估计:锁的持有窗口是微秒级,被枪毙的 5 个线程各自落在窗口内的概率约万分之一量级,<br>合计约 每次「停止挂机」千分之一。单次很低,但挂机脚本每天停启多次,累积起来是会遇到的。<br>后果:不是崩溃,是假死——需要在任务管理器里结束进程再重开。

两个可选加固(都不大,说一声我就加)

方案 A(推荐,最小改动):把 写日志 / 写封包日志 的 许可证_进入 换成<br>许可证_尝试进入 + 有限次重试;重试耗尽说明锁已异常,则放弃这一条日志直接返回。<br>再配一个 日志锁已失效 全局标志,一旦置真就永久走无锁追加分支。<br>效果:彻底不会假死,代价是异常状态下会丢掉个别日志行。<br>(关键点是「丢日志无害、写坏堆致命、假死最烦」——丢一条日志是最轻的代价。)

方案 B(治本,改动稍大):把 强制结束线程 换成协作式退出——<br>给每个线程一个 xxx线程退出 逻辑型标志,线程主循环每轮检查一次,自己 返回 退出。<br>这样线程永远死在安全点上,临界区自然不会被带走。这也顺带干掉工程里<br>「线程正执行置入代码机器码时被硬杀 → 游戏内部状态损坏闪退」那条老隐患<br>(第 11516 行注释提到的那个问题)。

补充:许可证_删除 本轮没有调用,因为工程没有窗口销毁事件,进程退出时操作系统会自动回收。
如果以后加了窗口销毁事件,可以在那里补一句 许可证_删除 (日志许可证)。

5. 怎么用 / 怎么回滚

建议先按 F7 编译一次确认「程序代码编译成功」,再实际挂机观察。

文本主本要回滚则用 _patch/日志加许可前_20261006_2238/ 里的两个文件覆盖回去。


6. 改动清单

文件位置改动
全局变量.e.txt末尾(第 106 行)新增 日志许可证, 许可证
代码/窗口程序集_MainDialogWnd.form.e.txt_MainDialogWnd_创建完毕 第 21~22 行启动线程前 许可证_创建 (日志许可证)
同上写日志入锁包住缓冲追加
同上写封包日志入锁包住缓冲追加
同上日志定时回调锁内只摘取清空,刷控件与落盘移出锁外
本页由 Markdown 报告自动转排;如需原始 md,可在原会话工作区取用。