D:/AI空间/rxjh/旧版本更新_修复后.e(2,742,760 字节,魔数 CNWTEPRG)D:/AI空间/rxjh/旧版本更新.代码_patch/fix_日志加线程许可.py旧版本更新_修复后.bak_日志加许可前_20261006_2238.e、_patch/日志加许可前_20261006_2238/(含改动前的 全局变量.e.txt、主本、项目.etprj)症状:挂机一段时间后程序无任何提示直接消失(不是报错退出、不是闪退提示,是进程没了)。
根因:写日志 全工程 681 个调用点、34 个工作线程入口,最终都只做这一句:
日志缓冲 = 日志缓冲 + 日志文本
日志缓冲 是全局文本型变量;界面线程装了 9971 定时器,每 500 毫秒执行一次:
新日志 = 日志缓冲
日志缓冲 = “”
两边都没有加锁。易语言里 A = A + B 是「取旧值 → 申请新块 → 拷贝 → 释放旧块 → 写回」五步非原子操作。两边交错执行时会出现:
两种都会破坏 Windows 堆结构。堆一旦损坏,后续任意一次内存分配都可能直接让进程消失 —— 这解释了「无提示、无日志、进程凭空不见」。
文件:旧版本更新.代码/全局变量.e.txt(末尾追加)
.全局变量 日志许可证, 许可证, , , 【线程许可】日志互斥锁(Windows 临界区):写日志/写封包日志 与 界面线程 日志定时回调 共用同一把;由 _MainDialogWnd_创建完毕 里 许可证_创建 建立
许可证 是薏米模块里的自定义类型(就是 LPCRITICAL_SECTION,6 个整数 = 24 字节),模块已把它和 许可证_创建 / 许可证_进入 / 许可证_离开 / 许可证_尝试进入 / 许可证_删除 一起 公开,<br>零新增声明即可直接用。
锁会指向垃圾内存——这是易语言里加锁最容易犯的错。
日志缓冲、封包日志缓冲 用同一把锁即可(单个锁、不嵌套,不存在死锁)。文件:代码/窗口程序集_MainDialogWnd.form.e.txt · _MainDialogWnd_创建完毕(第 21~23 行)
默认配置文件路径 = 全局目录 + “\Default.ini”
' 【线程许可】先建锁再起线程:日志锁必须早于任何会写日志的线程存在,全进程只创建这一次
许可证_创建 (日志许可证)
启动线程 (&自动进入游戏, , 自动进入游戏线程)
位置很关键:自动进入游戏 线程的第二行就调 写日志,下面第 24 行的 9971 定时器也会立刻开始跑。<br>所以建锁必须排在 启动线程 与 创建定时器 之前。全进程只创建一次,不要在用到的地方顺手创建<br>(那样每次都是一把新锁,等于没加)。
写日志:入锁包住「读取-拼接-写回」.子程序 写日志, , , 【写法优化】线程安全日志入口:…
.参数 日志文本, 文本型, , 要记录的日志内容(通常以 #换行符 结尾)
' 【线程许可】与界面线程 日志定时回调 争抢同一个全局文本变量:入锁保证“读取-拼接-写回”三步原子,
' 否则并发下同一块字符串内存被释放两次,堆被破坏,进程无任何提示直接消失
许可证_进入 (日志许可证)
日志缓冲 = 日志缓冲 + 日志文本
许可证_离开 (日志许可证)
许可证_进入 在拿不到锁时会一直等(不返回),拿到才往下走;许可证_离开 放行一个等待者。<br>681 个调用点一行没动——加锁这件事只发生在被调用的 写日志 内部。
写封包日志:同一把锁 ' 【线程许可】封包日志由 HOOK 回调线程写入,与界面线程定时器共用同一把锁
许可证_进入 (日志许可证)
封包日志缓冲 = 封包日志缓冲 + 日志文本
许可证_离开 (日志许可证)
封包日志缓冲 由被 HOOK 的那些线程写入、界面线程取走,是同一类竞态,所以一起收进同一把锁。
日志定时回调:锁内只摘取,慢操作全部挪到锁外这是本次最需要动脑的一处。原来的写法是「判断 → 取走 → 清空 → 刷控件 → 落盘」整段连着跑。<br>如果整段加锁,界面线程一次 写到文件 就会把所有工作线程的 写日志 全部堵住 → 整个挂机卡顿。
所以改成:锁内只做三行(取走 + 清空),剩下的控件刷新与落盘放到锁外。
' 【写法优化】本子程序由创建定时器(9971)安装在界面线程,每500毫秒触发一次,统一完成“日志显示+日志落盘”
' 【线程许可】只把“摘取并清空缓冲”这几行放进锁内;刷控件与落盘是慢操作,必须放到锁外,
' 否则所有工作线程的 写日志 都会被界面线程的一次文件写入挡住,表现为整个挂机卡顿
许可证_进入 (日志许可证)
新日志 = 日志缓冲
日志缓冲 = “”
许可证_离开 (日志许可证)
.如果真 (新日志 ≠ “”)
' 编辑框长度上限:超长只保留最近一半内容,防止长时间挂机内存与刷新开销无限增长
.如果真 (取文本长度 (挂机信息.内容) > 60000)
挂机信息.内容 = 取文本右边 (挂机信息.内容, 30000)
.如果真结束
挂机信息.加入文本 (新日志) ' 注意:此处必须直写控件(本子程序已在界面线程),不得改成写日志,否则日志会无限回环
' 增量落盘:只把本次新增的日志追加进文件,不再整框重写
日志路径 = 全局目录 + “\log\” + 全局_人物名称 + “.txt”
写到文件 (日志路径, 读入文件 (日志路径) + 到字节集 (新日志))
.如果真结束
许可证_进入 (日志许可证)
新日志 = 封包日志缓冲
封包日志缓冲 = “”
许可证_离开 (日志许可证)
.如果真 (新日志 ≠ “”)
.如果真 (取文本长度 (封包拦截框.内容) > 60000)
封包拦截框.内容 = 取文本右边 (封包拦截框.内容, 30000)
.如果真结束
封包拦截框.加入文本 (新日志)
.如果真结束
要点:
处理事件 一律不进锁。新日志、日志路径 沿用原有声明,第二段直接复用 新日志。这样改动的变量声明面为零,也就绕开了易语言「.局部变量 声明区不能被注释打断」的坑。
返回(),现在是走两次「空取空清」。效果完全相同,代价是两次进出锁(纳秒级)。
写日志(第 1591 行注释已说明必须直写控件),所以不存在自锁;即便万一重入,Windows 临界区对同一线程是可重入的(RecursionCount 递增),也不会死锁。
| # | 检查 | 结果 |
|---|---|---|
| 1 | 补丁自身断言(进出成对 4:4、建锁 1 处、写日志 调用点 681 不变、.如果真 块配平、含非 GBK 字符扫描) | 全部通过 |
| 2 | BOM / 换行 | 主本 BOM=True,CRLF=12799,LF-only=0(与改前一致) |
| 3 | t2e 回转 | 0 错误 0 警告,18480 条日志全部是「信息」级;SUCC:D:/AI空间/rxjh/_patch/out_lock.e |
| 4 | 项目.etprj | t2e 后已用 _patch/项目.etprj.pre12 还原,diff 与备份一致 |
| 5 | e2t 反查(把生成的 .e 再展开) | 4 组 许可证_进入 / 许可证_离开 全部是真代码(没有被降级成注释) |
| 6 | 分层保真 fidelity.py | L1 代码级 0 处不一致(466 个子程序全对);全局行代码一致 True;L2 注释变化 13 个子程序 / 42 条,全部是历史已知的行尾注释损耗,与本次改动无关 |
| 7 | 二进制确认 | 日志许可证 改前 0 次 → 改后 1 次;许可证_创建 1 → 2;4 处加锁点全部落地 |
改动范围(与 _patch/日志加许可前_20261006_2238/ 对比):只落在 4 个区段,+21 行 / −9 行,<br>没有波及其它任何子程序。
许可证_进入 是阻塞式的:拿不到锁就一直等。这把锁本身没问题,但它和工程里另一个既有做法<br>组合时会出现一个小概率故障:
停止挂机(第 11509 行起)在软停 5 秒后,会对 回补线程 / 回补下晶线程 / 挂机寻路线程 /<br>多坐标寻路线程 / 存卖石头线程 依次执行 强制结束线程(全工程共 25 处)。易语言这个命令<br>对应 Windows 的 TerminateThread —— 它不会释放被结束线程持有的临界区。
于是理论上存在这样一条路径:
某个线程刚好停在许可证_进入 … 许可证_离开这中间三个指令里被枪毙
→日志许可证永久停在「已进入」状态
→ 之后任何一次写日志都会永远等下去
→ 而停止挂机第 11560 行自己就调了一次写日志,且它跑在界面线程上
→ 界面线程卡死,整个程序假死(进程还在,窗口不动)。
概率估计:锁的持有窗口是微秒级,被枪毙的 5 个线程各自落在窗口内的概率约万分之一量级,<br>合计约 每次「停止挂机」千分之一。单次很低,但挂机脚本每天停启多次,累积起来是会遇到的。<br>后果:不是崩溃,是假死——需要在任务管理器里结束进程再重开。
方案 A(推荐,最小改动):把 写日志 / 写封包日志 的 许可证_进入 换成<br>许可证_尝试进入 + 有限次重试;重试耗尽说明锁已异常,则放弃这一条日志直接返回。<br>再配一个 日志锁已失效 全局标志,一旦置真就永久走无锁追加分支。<br>效果:彻底不会假死,代价是异常状态下会丢掉个别日志行。<br>(关键点是「丢日志无害、写坏堆致命、假死最烦」——丢一条日志是最轻的代价。)
方案 B(治本,改动稍大):把 强制结束线程 换成协作式退出——<br>给每个线程一个 xxx线程退出 逻辑型标志,线程主循环每轮检查一次,自己 返回 退出。<br>这样线程永远死在安全点上,临界区自然不会被带走。这也顺带干掉工程里<br>「线程正执行置入代码机器码时被硬杀 → 游戏内部状态损坏闪退」那条老隐患<br>(第 11516 行注释提到的那个问题)。
补充:许可证_删除本轮没有调用,因为工程没有窗口销毁事件,进程退出时操作系统会自动回收。
如果以后加了窗口销毁事件,可以在那里补一句许可证_删除 (日志许可证)。
D:/AI空间/rxjh/旧版本更新_修复后.e(不是 旧版本更新.e,那是过期副本)。建议先按 F7 编译一次确认「程序代码编译成功」,再实际挂机观察。
log\人物名.txt;连续挂机数小时不再出现「进程凭空消失」。旧版本更新_修复后.bak_日志加许可前_20261006_2238.e 覆盖回 旧版本更新_修复后.e 即可。文本主本要回滚则用 _patch/日志加许可前_20261006_2238/ 里的两个文件覆盖回去。
python _patch/fix_日志加线程许可.py(幂等,已打过会自己跳过)。| 文件 | 位置 | 改动 |
|---|---|---|
全局变量.e.txt | 末尾(第 106 行) | 新增 日志许可证, 许可证 |
代码/窗口程序集_MainDialogWnd.form.e.txt | _MainDialogWnd_创建完毕 第 21~22 行 | 启动线程前 许可证_创建 (日志许可证) |
| 同上 | 写日志 | 入锁包住缓冲追加 |
| 同上 | 写封包日志 | 入锁包住缓冲追加 |
| 同上 | 日志定时回调 | 锁内只摘取清空,刷控件与落盘移出锁外 |