Appearance
线程、设备会话与资源生命周期(V10)
线程和资源问题不只发生在“总体设计阶段”。实际上更多现场问题来自:第一次能运行,第二次启动失败;停止以后端口还被占用;重连时旧任务还在读;为了诊断又开了第二个设备会话,结果反而把原现场条件改变了。
V10 先判断当前任务,再决定是只追踪现有 owner、修一个生命周期 Bug,还是为新模块做完整设计。
1. 先分任务,不默认重画线程模型
| 当前问题 | 推荐路径 |
|---|---|
| “这个串口是谁打开的?”“Timer 在哪停?” | Read-Only Trace |
| 已明确的取消/释放小 Bug | Execution / MicroPatch |
| “以前能重复启动,最近第二次失败” | RegressionFix / known-good first |
| 偶发停止卡死、重试后恢复 | Feedback Ladder + 时间线 |
| 新设备模块、新采集流水线 | Discovery / Detailed Design |
| 多次出现同类资源泄漏、owner 分散 | 显式 Architecture Hotspot Review |
一个现有项目的 Dispose Bug 不需要先补完整总体设计文档。
2. 第一原则:每个长期资源都要有明确 Owner
需要能回答:
text
谁创建?
谁启动?
谁允许使用?
谁停止?
谁最终释放?
能不能重复创建?典型资源:
SerialPort;TcpClient/ Socket;- VISA / SCPI session;
- Modbus master;
- Camera / SDK handle;
- Timer;
- 后台 Task;
- Channel / Queue;
- FileStream;
- 数据库连接;
- 事件订阅;
- CancellationTokenSource。
最危险的状态不是“没有 Dispose”,而是多个地方都觉得自己拥有这个资源。
3. Device Session Ownership:不要为了方便再开第二条会话
很多仪器和工业设备在业务上应视为单会话资源:
text
Device Instance
↓
One Logical Session Owner
↓
all commands / queries / polling / health checks常见错误:
text
正常采集已经有一个 SCPI session
→ 异常恢复代码临时 new 第二个 client
→ 两个会话竞争设备状态/响应
→ 原问题变得更随机或者:
text
UI 连接按钮拥有 SerialPort
后台健康检查又自己 Open 同一个 COM诊断和恢复优先:
- 复用当前 session;
- 通过现有 owner 串行化请求;
- 使用日志 / Fake / Replay;
- 只有设备协议明确支持多 session 且设计本来如此,才允许第二会话。
不要把“为了排查方便”变成新的并发变量。
4. 资源状态不要只用 Connected / Disconnected
很多生命周期 Bug 来自状态太粗。
更有用的状态可能是:
text
Created
Connecting
Ready
Busy
Stopping
Disconnected
Faulted
Disposed对于设备平台还可能需要:
text
Available
Occupied
InUseByCurrentWorkflowUI 应展示操作员语义;内部生命周期可以更细。不要直接把 Task/线程内部状态原样暴露到主界面。
5. Start / Stop / Dispose 是三个不同动作
Start
需要明确:
- 是否允许重复调用;
- 已运行时再次 Start 是 no-op、报错还是重启;
- 前置连接是否必须 Ready;
- 启动失败后哪些资源要回滚。
Stop
Stop 的目标通常是:不再接受新工作,并让当前后台活动有序结束。
不一定等于立即 Dispose。
Dispose
Dispose 是资源所有权最终结束。Dispose 后是否允许重新创建新实例,是更上层 owner 的决定。
不要把每次“停止测试”都实现成销毁整个设备服务,除非项目明确就是这种生命周期。
6. 推荐停止顺序:先断工作流,再释放底层资源
通用思路:
text
Stop accepting new work
→ signal cancellation
→ stop timers / producers
→ wait or join relevant tasks
→ complete / drain queue as required
→ unsubscribe callbacks/events
→ close transport/session
→ dispose owned resources
→ publish final state具体顺序要结合库和设备协议,不要机械照抄。
关键问题是:
- 关闭底层 transport 时,后台 read 是否仍在运行;
- read 是否能响应 cancellation;
- callback 是否可能在 Dispose 后继续回来;
- 停止时队列剩余数据是丢弃还是必须落盘;
StopAsync是否可能等待自己;- UI 关闭是否同步阻塞等待后台任务。
7. Cancellation 要从上层传到底层,不要只改一层
典型错误:
text
Workflow 收到了 CancellationToken
→ Service 接口没有 token
→ SCPI read 固定等 10 秒
→ 用户点停止以后界面卡 10 秒应该检查最短链:
text
UI / Workflow
→ Application Service
→ Device Session
→ Transport Read/Write能取消的层尽量传递同一取消语义。
但是不要为了“所有方法都带 token”做全仓库机械重构;只沿当前阻塞链传播。
8. Timer、轮询和健康检查最容易造成重入
检查:
- 上一次轮询没结束,下一次是否又启动;
- Timer callback 是否并发执行;
- 页面关闭后是否还在刷新;
- 断线重连时是否旧轮询和新轮询同时存在;
- 健康检查是否和业务命令竞争同一 session;
- 测试窗口内是否应该暂停某些健康查询。
有些设备需要:
text
业务采集拥有会话
→ 健康检查读取缓存
→ 业务窗口结束后再恢复主动查询这种规则属于业务/资源所有权,不应该靠两个 Timer “谁先抢到谁用”。
9. Queue / Channel:先定义满了怎么办
不要只写“开后台线程保存”。
需要决定:
text
Capacity
Producer
Consumer
Backpressure
OverflowPolicy
StopPolicy满队列可以:
- 阻塞生产者;
- 丢最旧;
- 丢最新;
- 降采样;
- 报警;
- 触发故障。
不同策略会改变业务结果,因此是 Decision,不是实现细节。
停止时也要明确:
- 剩余数据必须写完;
- 可以丢弃;
- 给一个超时窗口后停止。
10. UI 线程:耗时工作不回 UI,状态更新才回 UI
常见原则:
text
通信 / 解析 / 保存 / 计算
→ background
Observable UI state update
→ UI thread重点避免:
- UI 线程同步
.Result/.Wait(); - 高频每样本 Dispatcher;
- 后台线程直接改绑定集合;
- window close 同步等待一个无法取消的 read;
- 日志无限追加导致 UI 本身成为性能瓶颈。
11. 事件订阅和 callback 是隐形资源
页面反复打开后越来越卡,常见原因不是线程,而是事件没有解绑。
检查:
text
Subscribe count
Unsubscribe owner
Window/ViewModel lifetime
Static event
SDK callback registration验收可以很简单:
text
打开页面 → 关闭 → 再打开
同一数据只刷新一次12. RegressionFix:资源问题如果“以前正常”,先看生命周期差异
不要先泛泛讨论死锁、GC、线程池。
先比较:
- 最近是否增加新 Timer;
- 是否改变取消 token;
- 是否把共享 session 改成局部 new;
- 是否调整了 Close / Dispose 顺序;
- 是否新增自动重连;
- 是否增加事件订阅;
- 是否改变页面/服务 scope;
- 是否改成 fire-and-forget Task。
建立:
text
KnownGood lifecycle
vs
Current lifecycle第一个行为差异比“当前代码看起来不优雅”更有价值。
13. 偶发卡死:先画时间线,不先猜锁
例如:
text
14:01:00.000 用户点击停止
14:01:00.005 Cancellation requested
14:01:00.010 polling task exits
14:01:00.015 raw read still pending
14:01:03.020 transport timeout
14:01:03.022 Dispose completes这里第一异常点很清楚:底层 read 没有及时响应停止。
之后才需要问:
- transport 本身不可取消?
- token 没传到底?
- Close 能否中断 read?
- 是否需要更短 read timeout + 上层 deadline?
14. 新模块的最小生命周期表
| Resource | Owner | Create | Start | Runtime | Stop | Dispose | Recreate |
|---|---|---|---|---|---|---|---|
| DeviceSession | DeviceService | 选择实例后 | Connect | 串行命令 | Cancel pending | Close/Dispose | Yes |
| PollingTask | DeviceService | Connect 后 | Ready | 周期读取 | Cancel | await end | Yes |
| SaveQueue | TestSession | StartTest | StartTest | enqueue | Complete | drain/close | Next test |
| UI Timer | ViewModel | View activate | View active | refresh | View close | Dispose | Yes |
表不要求所有资源都做得很复杂,但 owner 必须清楚。
15. 验证:生命周期 Bug 不靠“编译通过”证明
V1
局部 UI/事件订阅修改:
- affected build;
- 打开/关闭一次;
- 无重复 callback。
V2
设备 session / reconnect:
- Fake / Replay;
- Connect → Disconnect → Reconnect;
- Stop 后能再次 Start;
- 不出现第二 session。
V3
状态机/并发/测试流程:
text
Start
→ Running
→ Cancel/Stop
→ all tasks end
→ resources released or retained as designed
→ Restart再加异常路径:
- stop during read;
- disconnect during workflow;
- close UI during reconnect;
- queue not empty;
- device fault during stop。
真实危险设备动作仍需人工验收。
16. 两个可直接复制的 Prompt
查 owner,不修改
text
$host-computer-dev
只做 Read-Only Trace。
我想确认【某 SerialPort / TCP / SCPI session / Timer / Task】的生命周期。
请追踪:
1. 谁创建;
2. 谁持有;
3. 谁启动;
4. 谁停止;
5. 谁 Dispose;
6. 是否可能存在第二实例或重复订阅;
7. Stop 后是否允许再次 Start。
不要修改代码、构建或重画全项目架构。修复停止/重连问题
text
$host-computer-dev
这是一个资源生命周期问题。
现象:
【例如:第一次测试正常,停止后第二次连接提示端口占用】
是否以前正常:
【是 / 否】
请先建立当前 owner 和 Start/Stop/Dispose 时间线。
如果以前正常,先比较 known-good 生命周期差异。
优先检查重复 session、未退出 read、Timer/事件残留、取消未传播和 Dispose 顺序。
不要为了诊断额外打开第二个真实设备会话。
修复后用同一反馈信号验证 Stop → Restart / Reconnect。17. 常见反模式
| 反模式 | 后果 |
|---|---|
| 多个模块各自 new 一个设备 client | session 竞争、状态不可控 |
| Stop = 直接 Dispose 一切 | 业务级停止与资源生命周期混淆 |
| Dispose 时后台 read 仍在跑 | 异常、卡死、资源占用 |
| 重连新任务启动,旧任务没退出 | 双轮询、双回调 |
| 每次 Timer tick 都 fire-and-forget | 重入和任务堆积 |
| 页面关闭不解绑事件 | 重复刷新和内存泄漏 |
| 用户说以前正常却不看 Git | 错过真正 lifecycle delta |
| 为一个局部 Bug 重写整个并发模型 | 风险和工时失控 |
下一步
- 通信实现与协议:串口 / TCP 通信任务模板
- 通信现场证据:通信证据包
- 回归和偶发问题:Bug / 回归定位
- WPF 状态与 UI 线程:WPF 上位机 AI 开发流程
- 新系统完整详细设计:详细设计总览
一句话原则
资源只让一个 owner 真正负责;停止先结束工作,再按设计释放资源;诊断不要偷偷引入第二个设备会话。