Skip to content

线程、设备会话与资源生命周期(V10)

内容类型:任务分流 + 生命周期设计/诊断难度:进阶适合:WPF/WinForms、设备通信、长时间运行、停止/重连、资源占用和并发问题阅读时间:约 12 分钟

线程和资源问题不只发生在“总体设计阶段”。实际上更多现场问题来自:第一次能运行,第二次启动失败;停止以后端口还被占用;重连时旧任务还在读;为了诊断又开了第二个设备会话,结果反而把原现场条件改变了。

V10 先判断当前任务,再决定是只追踪现有 owner、修一个生命周期 Bug,还是为新模块做完整设计。

1. 先分任务,不默认重画线程模型

当前问题推荐路径
“这个串口是谁打开的?”“Timer 在哪停?”Read-Only Trace
已明确的取消/释放小 BugExecution / 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

诊断和恢复优先:

  1. 复用当前 session;
  2. 通过现有 owner 串行化请求;
  3. 使用日志 / Fake / Replay;
  4. 只有设备协议明确支持多 session 且设计本来如此,才允许第二会话。

不要把“为了排查方便”变成新的并发变量。

4. 资源状态不要只用 Connected / Disconnected

很多生命周期 Bug 来自状态太粗。

更有用的状态可能是:

text
Created
Connecting
Ready
Busy
Stopping
Disconnected
Faulted
Disposed

对于设备平台还可能需要:

text
Available
Occupied
InUseByCurrentWorkflow

UI 应展示操作员语义;内部生命周期可以更细。不要直接把 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. 新模块的最小生命周期表

ResourceOwnerCreateStartRuntimeStopDisposeRecreate
DeviceSessionDeviceService选择实例后Connect串行命令Cancel pendingClose/DisposeYes
PollingTaskDeviceServiceConnect 后Ready周期读取Cancelawait endYes
SaveQueueTestSessionStartTestStartTestenqueueCompletedrain/closeNext test
UI TimerViewModelView activateView activerefreshView closeDisposeYes

表不要求所有资源都做得很复杂,但 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 一个设备 clientsession 竞争、状态不可控
Stop = 直接 Dispose 一切业务级停止与资源生命周期混淆
Dispose 时后台 read 仍在跑异常、卡死、资源占用
重连新任务启动,旧任务没退出双轮询、双回调
每次 Timer tick 都 fire-and-forget重入和任务堆积
页面关闭不解绑事件重复刷新和内存泄漏
用户说以前正常却不看 Git错过真正 lifecycle delta
为一个局部 Bug 重写整个并发模型风险和工时失控

下一步

一句话原则

资源只让一个 owner 真正负责;停止先结束工作,再按设计释放资源;诊断不要偷偷引入第二个设备会话。

别来无恙 · C# 上位机 AI 实战站 · 从零到交付 · QQ 群:1016188499