把 Modbus 轮询塞进 Trio 的异步循环:内存映射与定时采集实战
关键词:CustomTkinter · Trio · Modbus TCP · 结构化并发 · 共享状态层 · 上位机一、为什么把 Modbus 轮询托管给 Trio工业上位机的经典形态是「主线程驱动 GUI、独立线程执行采集」:基于 tkinter/CustomTkinter 渲染界面,开启一个 threading.Thread 循环读取保持寄存器,再通过 queue.Queue 将样本回传主线程刷新控件。该范式可运行,但在生产环境下存在三处结构性缺陷:生命周期不可控。线程一旦进入 while True,只能依赖 threading.Event 等标志位轮询退出,取消路径容易被遗漏,进而残留不可回收的僵尸线程。故障域不隔离。任一从站掉线或单次读取超时,异常常在线程体内被静默吞噬,表现层呈现「界面卡死」假象,而底层采集协程早已崩溃。并发编排脆弱。多从站、异构轮询周期、存在依赖关系的任务,用线程加锁手工拼装,缺乏统一的作用域管理,可维护性随时间急剧劣化。Trio 的结构化并发(structured concurrency)模型恰好针对上述问题:所有并发单元都存活于 nursery 作用域内,作用域退出时保证其内部任务被确定性地全部回收;任一任务抛出未捕获异常,整个作用域可通过取消作用域(cancel scope)被统一撤销。本文以一个最小可运行示例,演示如何将 Modbus 轮询真正纳入 Trio 的事件循环,并通过一层共享状态层(shared-state / 内存映射)实现采集与表现的彻底解耦。二、核心架构:以共享状态层桥接采集与表现采集单元与表现单元之间最危险的是直接耦合——采集线程直接改写 UI 控件变量,导致界面重构必须连带修改协议逻辑。正确做法是引入一层共享状态层(in-memory data model):它是一个受互斥锁保护的领域对象,采集侧只负责写入,表现侧只负责读取,两侧互不感知,依赖关系被该层彻底切断。在同进程内,无需引入 multiprocessing.shared_memory,一个受 trio.Lock 保护的 dataclass 即满足需求: