modbus 49 线程 modbusmaster

线程

1. 什么是「线程资源」?

线程 理解成一个「干活的工人」

线程资源就是这个工人干活时用到的「工具、材料、场地」:

  • Task tCheck 就是那个工人(后台采集线程),它要做的事是:循环读 Modbus 数据 → 更新 UI → 报警判断。
  • 它用到的「资源」包括:
    • CancellationTokenSource source:用来喊「停」的哨子
    • _modbusHelper:串口通信工具
    • serialPort:物理串口资源
    • 内存、CPU 时间片等系统资源

如果不释放线程资源:工人下班了还占着工具、场地,下次再喊他干活就没工具用了 → 表现为:串口被占用、程序关不掉、后台线程还在跑。


2. 什么是「线程控制」?

「线程控制」不是一个变量,而是一套控制线程「启动/暂停/停止」的逻辑,核心是:

  • 让后台采集线程只在需要时运行(连接设备后启动)
  • 让后台采集线程能被安全停止(断开连接/关闭程序时)

「线程控制」的关键部分:

csharp 复制代码
Task tCheck = null;                  // 代表后台采集线程(工人)
CancellationTokenSource source;     // 用来发「停止」信号的工具
CancellationToken token = source.Token; // 具体的「停止」信号

1. 变量声明(Form1 开头)

csharp 复制代码
Task tCheck = null;                  // 后台采集线程(工人)
CancellationTokenSource source = new CancellationTokenSource(); // 哨子
bool isFirstRead = false;            // 标记是否第一次采集
  • tCheck:用来引用后台线程,方便你知道线程是否在运行
  • source:用来发停止信号,让线程安全退出
  • isFirstRead:控制第一次采集时是否要读设备状态

2. 启动线程(StartMonitor 方法)

csharp 复制代码
private void StartMonitor()
{
    // 重置哨子(避免上次的信号干扰)
    source = new CancellationTokenSource();
    CancellationToken token = source.Token;

    // 启动工人(后台线程)
    tCheck = Task.Run(async () =>
    {
        // 循环:只要没收到停止信号,就一直采集
        while (!token.IsCancellationRequested)
        {
            try
            {
                // 采集逻辑:读Modbus、更新UI、报警判断
                await Task.Delay(1000, token); // 1秒采集一次,延时也会响应停止信号
            }
            catch (Exception ex)
            {
                // 采集失败时,延时后继续
                await Task.Delay(1000, token);
            }
        }
    }, token);
}
  • Task.Run(...):把采集逻辑放到后台线程,不卡 UI
  • while (!token.IsCancellationRequested):线程的「主循环」,只要没收到停止信号就一直跑
  • await Task.Delay(1000, token):既控制采集间隔,又能在延时期间响应停止信号

3. 停止线程(butConnect_Click 断开连接时)

csharp 复制代码
if (_modbusHelper.IsOpen)
{
    // 吹哨子:让后台线程停止
    source.Cancel();
    // 关闭串口,释放资源
    _modbusHelper.Close();
    // 重置状态
    isFirstRead = false;
    localDeviceRunning = false;
}
  • source.Cancel():给所有用这个 source 创建的 token 发「停止」信号
  • 后台线程里的 while (!token.IsCancellationRequested) 会立刻变成 false,循环结束,线程安全退出

三、为什么要这么设计?(避免踩坑)

1. 为什么不能直接 tCheck = null 停止线程?

  • 线程是操作系统调度的,你把 tCheck = null 只是丢掉了引用,工人还在后台干活 → 串口还占着、CPU 还在跑
  • 必须用 source.Cancel() 发信号,让线程自己主动结束循环,才是安全的

2. 为什么要 source.Dispose()?(在 Dispose 方法里)

csharp 复制代码
protected override void Dispose(bool disposing)
{
    if (disposing)
    {
        source?.Cancel();       // 先吹哨子
        source?.Dispose();      // 再销毁哨子,释放资源
        _modbusHelper?.Dispose();
        components?.Dispose();
    }
    base.Dispose(disposing);
}
  • source 本身也是一个「资源」,用完要销毁,避免内存泄漏
  • 顺序很重要:先 Cancel() 让线程停,再 Dispose() 销毁哨子

终极正确顺序(Dispose 里必须这么来)

先 cts.Cancel() → 把后台采集循环立刻停掉

(循环一停,就不会再读串口、更 UI,从根源防报错)

再 SerialPort.Close() → 关闭串口硬件

最后释放其他资源

ModbusHelper.Dispose()

Timer.Dispose()

❌ 为什么不能先关串口、再 Cancel?

如果你先 Close() 串口,后台循环还在跑,它还会去读串口,直接报:

端口已关闭 / 访问已释放资源 错误!

先停循环 → 再关串口 → 再放资源 → 最后关窗体

绝对不允许直接关窗体,否则必报:跨线程错误、端口占用、内存泄漏。

标准 4 步流程
  1. 先取消后台循环(用CancellationTokenSource)
    cts就是你的取消令牌,cts.Token是停止指示器
    调用cts.Cancel(),立刻停止所有后台循环(数据采集、Modbus 轮询、UI 刷新)
    作用:不让后台再碰 UI 控件,从根源杜绝跨线程崩溃
  2. 关闭串口硬件(SerialPort)
    串口是硬件资源,必须先Close()
    不 Close 就关窗体,直接报 "端口被占用",下次连不上
  3. 释放所有托管 / 非托管资源
    Modbus 相关:ModbusHelper → Dispose()
    定时器:Timer → Dispose()
    其他后台对象:全部释放
    作用:不占内存、不占线程、不留垃圾
  4. 最后关闭窗体
    所有资源都放干净了,再关窗体,绝对安全
    三、取消令牌大白话总结(你说得全对)
    CancellationTokenSource(cts) = 遥控器
    Token = 停止信号
    Cancel() = 按 "关机键",直接断电停循环

3. CancellationTokenSource source 到底是什么?

这是 C# 里专门用来安全停止线程/异步任务的工具,你可以把它理解成:

一个「哨子 + 信号接收器」:

  • 你吹哨子(source.Cancel()),工人(线程)就收到「停止干活」的信号
  • 工人干活时会时不时摸一下口袋(token.IsCancellationRequested),如果收到信号就立刻停手

核心用法(对应逻辑)

csharp 复制代码
// 1. 创建哨子(初始化)
source = new CancellationTokenSource();
CancellationToken token = source.Token;

// 2. 让工人(线程)带着哨子干活
tCheck = Task.Run(async () =>
{
    // 循环干活:只要没收到停止信号,就一直采集
    while (!token.IsCancellationRequested)
    {
        // 读Modbus数据、更新UI...
        await Task.Delay(1000, token); // 延时也会听哨子
    }
}, token);

// 3. 吹哨子(停止线程)
source.Cancel();

四、总结成一句话

  • 线程资源:后台线程干活用到的所有东西(串口、内存、信号工具等)
  • 线程控制 :用 CancellationTokenSource 这套工具,安全地让后台采集线程「启动/停止」
  • CancellationTokenSource source:就是那个「哨子」,用来喊后台线程「别干了,收工」

ModbusSerialHelper 与ModbusMaster

先明确:「两个对象的分工」

ModbusSerialHelper _modbusHelper = new ModbusSerialHelper();封装的「Modbus通信容器」,这个类的核心职责就两个:

  1. 管串口连接 :打开/关闭串口、配置波特率/串口号、管理串口资源(对应Open()/Close()/IsOpen);
  2. 存核心通信工具 :把Modbus协议的「指令收发器」------ModbusMaster,封装成自己的公共属性public IModbusMaster ModbusMaster { get; private set; })。

简单说:_modbusHelper 是**"通信终端机"ModbusMaster 是这台终端机里的"核心指令收发器"**------终端机负责连网线(串口),收发器负责发/收和设备的对话指令(读线圈、读寄存器、写指令)。


一、ModbusMaster (核心:所有Modbus读写操作,必须通过它执行!

这是最关键的一点:_modbusHelper只负责「连串口」,但不会直接和设备发/收Modbus指令------真正执行「读线圈、读寄存器、写线圈、写寄存器」的,全是ModbusMaster

代码里所有和设备的通信代码,底层都是调用ModbusMaster的方法,比如:

  1. 读设备启停状态(线圈1):_modbusHelper.ModbusMaster.ReadCoilsAsync(...) → 调用ModbusMaster读线圈方法
  2. 读温度寄存器(寄存器1):_modbusHelper.ModbusMaster.ReadHoldingRegistersAsync(...) → 调用ModbusMaster读保持寄存器方法
  3. 启动设备(写线圈1):_modbusHelper.ModbusMaster.WriteSingleCoilAsync(...) → 调用ModbusMaster写单个线圈方法
  4. 写频率寄存器(寄存器20):_modbusHelper.ModbusMaster.WriteMultipleRegistersAsync(...) → 调用ModbusMaster写多个寄存器方法

👉 一句话总结:ModbusMaster是Modbus协议的「指令执行器」,没有它,你就算串口连成功了,也没法和设备说一句话(读/写全失效) ;而你封装的_modbusHelper,只是把这个"执行器"和"串口连接"打包在一起,方便管理而已。


二、为啥要调_modbusHelper.ModbusMaster这个属性?(因把"执行器"藏在"终端机"里了)

ModbusSerialHelper类里,把ModbusMaster定义成了自己的私有赋值、公共读取的属性get; private set;):

csharp 复制代码
// ModbusSerialHelper.cs里的代码
public IModbusMaster ModbusMaster { get; private set; }
  • private set:只有ModbusSerialHelper自己能给这个属性赋值(比如Open()方法里ModbusMaster = ModbusSerialMaster.CreateRtu(_serialPort);),外部(比如Form1)不能改;
  • public get:外部(Form1)可以读取这个属性的值,拿到里面的"指令执行器"。

在Form1里调_modbusHelper.ModbusMaster,本质就是从封装的"通信终端机"里,把藏在里面的"指令执行器"拿出来用 ------不调这个属性,你根本拿不到ModbusMaster,自然没法和设备通信。


三、为啥要把_modbusHelper.ModbusMaster赋值给Form1里的master变量?(不是必须,但代码更简洁、维护更方便

既然能直接用_modbusHelper.ModbusMaster.XXX(),为啥还要多一步master = _modbusHelper.ModbusMaster;
答案:这步赋值「不是语法必须」,是「代码写法的优化」 ,完全可以删掉Form1里的master变量,所有地方都用_modbusHelper.ModbusMaster,功能完全一样!

比如代码里的:

csharp 复制代码
// 赋值后用master
await master.ReadHoldingRegistersAsync(1,1,2);
// 不赋值,直接用_modbusHelper.ModbusMaster,效果完全一样
await _modbusHelper.ModbusMaster.ReadHoldingRegistersAsync(1,1,2);

那为啥要多这一步?两个实际好处(工业代码的常用写法):

1. 简化代码,少写重复字符

不用每次都写长长的_modbusHelper.ModbusMaster.XXX,直接写master.XXX,代码更短、更易读;

2. 降低耦合,方便后续修改

如果后续你想改ModbusSerialHelper里的属性名(比如把ModbusMaster改成Master),只需要改赋值那一行master = _modbusHelper.Master;),不用改Form1里所有的读写方法;

如果后续想换Modbus通信的封装类,也只需要改赋值行,不用动所有通信代码。

👉 简单说:Form1里的master变量,就是_modbusHelper.ModbusMaster的**"快捷方式"**,不用白不用,用了更方便。


总结:代码里的通信逻辑链(从新建对象到和设备对话)

新建_modbusHelper → 调用_modbusHelper.Open()连串口 → Open()里给_modbusHelper.ModbusMaster赋值(创建指令执行器) → Form1里读取_modbusHelper.ModbusMaster(拿出执行器)→ 要么直接用,要么赋值给master当快捷方式 → 通过ModbusMaster执行读/写指令,和设备通信

所有环节的核心,都是为了拿到**ModbusMaster这个「指令执行器」------你的_modbusHelper只是它的"容器+连接管家"而已。
ModbusMaster就是你和设备通信的
"唯一入口"**,其他都是为了让它能正常工作的辅助!

相关推荐
fundoit5 小时前
Modbus调试软件实战指南:从基础连接到高级脚本的全方位调试方案
modbus·mthings·调试工具
爱凤的小光2 天前
Modbus协议指南---个人学习笔记
modbus
麦德泽特3 天前
基于 Go 语言的 Modbus 项目实战:构建高性能、可扩展的工业通信服务器
服务器·开发语言·golang·modbus·rtu
李庆政37011 天前
modbus协议四 rtu Over tcp & mbslave & CRC校验码计算方法
网络协议·tcp/ip·modbus·rtu over tcp
疆鸿智能研发小助手17 天前
PROFINET转MODBUS TCP网关接安科瑞马达保护器案例
modbus·马达保护器·工业自动化·profinet·工业通讯·协议转换网关
疆鸿智能研发小助手18 天前
破局机床精密加工:疆鸿智能PROFINET转MODBUS TCP通讯壁垒终结者
modbus·modbus tcp·工业自动化·profinet·工业通讯·协议转换网关·智控机床
謓泽19 天前
【MODBUS】串口 RTU / Modbus TCP / 透明就绪
网络·串口·modbus
疆鸿智能研发小助手22 天前
疆鸿智能ETHERNET IP转MODBUS,让施耐德变频器“对话”无界
modbus·工业自动化·变频器·ethernet ip·工业通讯·协议转换网关
czhc11400756631 个月前
Modbus wpf 35
modbus