Appearance
案例 A-01:串口原始数据异步保存
本案例的关键不是“文件必须用 Channel 写”,而是:在不破坏接收、解析和 Stop 生命周期的前提下,保留可追溯的原始数据。
1. 原始需求
项目希望在协议解析之前保存接收到的原始字节,用于现场问题回放和追溯。
业务约束可以整理成:
text
Record:
保留原始 RX 数据及必要时间/连接上下文
NonGoals:
不改变协议解析语义
不因为 UI 显示需要而裁剪正式保存数据还需要项目明确一项关键决策:
原始记录失败时,是
AlarmOnly / Advisory继续采集,还是因为法规/验收要求必须停止?
不能默认所有项目都“保存失败但采集继续”。
2. 先确认接收路径和 Buffer Ownership
System.IO.Ports.SerialPort.DataReceived 事件本身并不会直接把一个业务 byte[] 参数交给应用。项目通常会在回调里读取 BytesToRead / Read(),或者调用自己的接收封装。
因此真正要问的是:
text
Receive Source
→ 谁创建 buffer?
→ buffer 是否复用?
→ 什么时候数据所有权转移?
→ 异步消费者拿到的是独立副本还是共享内存?只有当项目自己的 buffer 会在下一次读取中复用时,才需要在跨异步边界前复制。
所以更准确的规则是:
跨生命周期/线程边界前,要保证数据所有权清楚;需要时复制,不是看到 DataReceived 就机械 Clone。
3. 为什么“直接 Task.Run 写文件”不一定成立
把每次收到的数据都 Task.Run 一次,可能引入:
- 多任务并发写同一流,顺序和互斥变复杂;
- 任务数量在高频输入下持续增长;
- Stop 时仍有未完成 writer;
- 异常散落在多个后台任务中;
- 共享 buffer 所有权不清时出现数据污染。
但这不意味着 Task.Run 在任何项目里都错误。关键是有没有稳定的 writer ownership 和可证明的停止行为。
4. 本案例采用生产者/消费者思路
案例中的目标结构是:
text
Receive Owner
→ immutable/copy buffer(按需要)
→ queue/channel
→ single writer owner
→ file这样可以把:
- 设备接收;
- 协议解析;
- 原始记录;
分开验证。
Channel 的正确理解
System.Threading.Channels 是一种实现选择,不是规则本身。
- Bounded Channel 可以配置容量和满载策略,因此能形成可控背压;
- Unbounded Channel 不会自动解决生产速度长期大于消费速度的问题;
- 即使用 bounded channel,也必须决定“满了怎么办”:等待、丢弃、报警还是停止。
真正应该写入设计的是 Queue Policy,而不是一句“使用 Channel 即可”。
5. 关闭流程不能只写一个顺序模板
案例中常见的安全顺序是:
text
停止产生新数据
→ 结束/取消接收
→ 通知 writer 不再有新项
→ 按项目策略 drain 或 cancel
→ flush
→ dispose file/session但要注意两个差异:
Drain
适合“必须尽量把已接收数据落盘”的项目。
Cancel
适合紧急停止、快速退出或数据允许丢弃的项目。
不要把“Stop 一定必须等待所有数据写完”写成绝对规则。真正的行为应由数据重要性、退出时限和安全要求共同决定。
对应方法:线程、设备会话与资源生命周期。
6. 保存失败的事件后果要显式定义
可能的项目策略:
| 场景 | 示例后果 |
|---|---|
| 原始日志只是诊断辅助 | AlarmOnly / Advisory,采集继续 |
| 原始记录属于验收必需证据 | 可能阻止开始或停止当前任务 |
| 磁盘空间预警 | Advisory + fail-open |
| 写入过程中真正磁盘满 | 根据数据完整性要求决定继续/停止 |
所以“写文件异常只记日志、不向上抛”不能被当成通用最佳实践。
7. 原始记录需要什么上下文
只保存裸字节有时不够。现场追溯通常至少考虑:
text
Timestamp
Direction (RX/TX)
Session / Device
Length
Raw Payload
必要时的 Sequence / Command Context如果协议包含敏感字段,则按项目安全规则脱敏,不把原始协议直接复制到公共资料。
8. 验证按本项目协议和负载选择
不要固定要求所有串口任务都测“半包、粘包、CRC”。这些只在当前协议确实存在相关机制时才有意义。
V2 focused
可以验证:
- RX 原始数据与文件记录一致;
- 顺序策略符合要求;
- writer 异常被正确报告;
- Stop 后没有旧 writer/session 残留;
- Fake/Replay 下协议解析结果未改变。
Performance / Long-run
只有存在吞吐或长稳目标时再测:
- queue depth;
- write throughput;
- 内存增长;
- 72h 等实际要求。
没有做过长期测试就保留 Unverified,不要写“内存稳定”。
HardwarePending
真实设备持续高频输入、断电、驱动行为等场景需要现场条件时,明确保持 HardwarePending。
9. 本案例曾采用 Channel,但不要把实现写成规则
案例里使用过类似:
text
receive
→ copy/owned buffer
→ Channel
→ single writer它解决了当时项目的并发和顺序问题。
但换一个项目,现有成熟 writer、批量缓冲、MemoryMappedFile、数据库批处理或设备 SDK 自带记录机制都可能更合适。
V10 优先复用已有稳定 seam,只有当前实现确实无法满足要求时才引入新抽象。
10. 最值得保留的经验
- 先确认 Buffer Ownership。 不把 SerialPort API 和项目自己的缓冲区机制混为一谈。
- 接收路径避免不必要的慢操作。 但“哪些操作太慢”用实际负载判断。
- Queue Policy 比 Queue 类型更重要。 容量、满载、顺序、丢弃和停止策略必须明确。
- 保存失败后果属于业务语义。 不默认 fail-open,也不默认停机。
- Stop 需要 owner 和 lifecycle。 不留下旧 writer、重复 session 和悬挂任务。
- 原始记录是证据。 最好能进入 Replay,而不只是“保存了一个 bin 文件”。