LOGO OA教程 ERP教程 模切知识交流 PMS教程 CRM教程 开发文档 其他文档  
 
网站管理员

用async/await + Action/Func<Task>实现异步操作,避免 UI 卡死 WinForm 程序

admin
2026年1月24日 11:40 本文热度 68

先明确整体背景

这个示例是一个WinForm 工控上位机程序,核心功能是通过Modbus TCP 协议和 PLC(或 Modbus 模拟器)通信,全程用async/await + Action/Func<Task>实现异步操作,避免 UI 卡死 —— 这是工控上位机开发的核心要求(UI 必须流畅,通信不能阻塞)。

一、前置基础配置(程序初始化)



1
2
3
4
5
6
7
8
9
10
11
12

// Modbus TCP配置(根据实际PLC修改)
private readonly string _plcIp = "192.168.1.100"; // PLC/模拟器IP
private readonly int _plcPort = 502; // Modbus TCP默认端口
private readonly int _startAddress = 40001; // 保持寄存器起始地址(4x区)
private readonly int _registerCount = 3; // 读取寄存器数量
 
public Form1()
{
    InitializeComponent();
    txtLog.Multiline = true;
    txtLog.ScrollBars = ScrollBars.Vertical;
}



关键说明:

  1. 1. Modbus 配置参数
    • • _plcIp/_plcPort:PLC 的网络地址,Modbus TCP 默认端口是 502(几乎所有 PLC / 模拟器都用这个);
    • • _startAddress:Modbus 保持寄存器(4x 区)的起始地址,工控中 40001 是最常用的起始地址(对应 PLC 的模拟量 / 数值型数据);
    • • _registerCount:要读取的寄存器数量(这里读 3 个,分别存温度、压力、运行状态)。
  2. 2. UI 初始化:设置日志文本框为 “多行 + 滚动条”,是工控上位机日志显示的标准配置。

二、核心工具方法:线程安全的日志输出



1
2
3
4
5
6
7
8
9
10

private void Log(string message)
{
    if (txtLog.InvokeRequired)
    {
        Action<string> logAction = Log;
        txtLog.Invoke(logAction, message);
        return;
    }
    txtLog.AppendText($"[{DateTime.Now:HH:mm:ss.fff}] {message}{Environment.NewLine}");
}



关键说明(工控开发必懂):

  1. 1. 为什么需要这个方法?
    异步任务(如 Modbus 通信)运行在后台线程,WinForm 的 UI 控件(如 txtLog)只能由UI 线程更新,直接在后台线程改 UI 会抛异常 —— 这是 WinForm 跨线程更新 UI 的核心坑。
  2. 2. 核心逻辑
    • • txtLog.InvokeRequired:判断当前线程是不是 UI 线程;
    • • Action<string> logAction = Log:把 Log 方法封装成Action委托;
    • • txtLog.Invoke(logAction, message):让 UI 线程执行这个 Action,实现线程安全的 UI 更新。
  3. 3. 工控场景价值:上位机需要实时显示通信日志、设备状态,这个方法是所有 UI 更新的基础,保证日志不会 “卡壳” 或报错。

三、场景 1:异步读取 PLC Modbus TCP 数据(核心功能)

这是整个示例的核心,拆解成 5 个步骤讲解:

步骤 1:按钮点击事件(UI 触发入口)



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

private async void btnReadPLC_Click(object sender, EventArgs e)
{
    btnReadPLC.Enabled = false; // 禁用按钮,防止重复点击(工控必做)
    Log($"开始连接PLC:IP={_plcIp}, 端口={_plcPort}");
 
    try
    {
        // 封装Modbus读取逻辑为异步委托
        Func<Task<string>> readPlcLogic = async () =>
        {
            // 异步执行Modbus通信(下面讲)
        };
 
        // 异步执行,不阻塞UI
        string plcData = await readPlcLogic();
        lblPLCData.Text = plcData; // 更新PLC数据显示
        Log($"PLC数据读取完成:{plcData}");
    }
    catch (Exception ex)
    {
        // 异常捕获(工控中必须有,通信失败要提示)
        Log($"读取PLC失败:{ex.Message}");
        MessageBox.Show($"Modbus通信异常:{ex.Message}", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
    }
    finally
    {
        btnReadPLC.Enabled = true; // 恢复按钮
    }
}



关键说明:

  • • async void:仅用于UI 事件处理程序(如按钮点击),这是 C# 允许async void的唯一安全场景;
  • • btnReadPLC.Enabled = false:工控中必须做 —— 防止用户快速多次点击,导致同时建立多个 PLC 连接,引发端口占用 / 通信混乱;
  • • Func<Task<string>> readPlcLogic:把 Modbus 读取逻辑封装成异步委托(有返回值,返回 PLC 数据字符串),替代不能直接用的async Action

步骤 2:Modbus 通信核心逻辑(readPlcLogic 内部)



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

return await Task.Run(() =>
{
    TcpClient tcpClient = null;
    IModbusMaster modbusMaster = null;
    try
    {
        // 1. 建立TCP连接
        tcpClient = new TcpClient();
        tcpClient.Connect(_plcIp, _plcPort);
        Log("PLC TCP连接成功");
 
        // 2. 创建Modbus主站
        modbusMaster = ModbusIpMaster.CreateIp(tcpClient);
        modbusMaster.Transport.ReadTimeout = 2000; // 读取超时2秒
 
        // 3. 读取保持寄存器(地址偏移转换是核心!)
        int startOffset = _startAddress - 40001; // 40001 → 偏移0
        ushort[] registers = modbusMaster.ReadHoldingRegisters(
            slaveAddress: 1, // 从站地址(PLC站号,默认1)
            startAddress: startOffset,
            numberOfPoints: _registerCount);
 
        // 4. 解析寄存器数据(工控数据解析的典型方式)
        float temperature = registers[0] / 10.0f; // 寄存器值/10 = 实际温度
        float pressure = registers[1] / 100.0f;   // 寄存器值/100 = 实际压力
        int status = registers[2];                // 运行状态(0=停止,1=运行)
 
        return $"温度={temperature}℃,压力={pressure}MPa,运行状态={(status == 1 ? "正常" : "停止")}";
    }
    finally
    {
        // 5. 释放资源(工控中必须做,否则会占用TCP端口)
        modbusMaster?.Dispose();
        tcpClient?.Close();
        tcpClient?.Dispose();
    }
});



关键说明(工控开发重点):

  1. 1. TCP 连接建立
    • • TcpClient.Connect:和 PLC 建立 TCP 连接,这是 Modbus TCP 的基础(Modbus TCP 本质是基于 TCP/IP 的应用层协议);
    • • 必须放在Task.Run里:因为Connect是同步阻塞操作,放异步委托里避免 UI 卡死。
  2. 2. **Modbus 地址偏移转换(最易错点)**:
    • • PLC 手册里的地址是40001,但 NModbus4 要求传偏移量(从 0 开始),所以要减 40001;
    • • 比如 40001→0、40002→1、40003→2,这是所有 Modbus 库的通用规则,错了就读不到数据!
  3. 3. 寄存器数据解析
    • • PLC 的寄存器是ushort(无符号 16 位整数),工控中不会直接存浮点数(如 25.6℃),而是存整数(256),上位机再除以 10/100 解析 —— 这是为了减少 PLC 数据传输开销,是行业惯例;
    • • 运行状态用 0/1 表示,是工控中 “离散状态” 的典型存储方式。
  4. 4. 资源释放
    • • finally块里释放modbusMastertcpClient:工控现场 PLC 连接数有限,不释放会导致 “连接耗尽”,PLC 拒绝新连接,这是线上故障的高频原因!

四、场景 2:异步批量下发 Modbus 指令(写入寄存器)



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59

private async void btnSendCommands_Click(object sender, EventArgs e)
{
    btnSendCommands.Enabled = false;
    Log("开始批量下发Modbus指令到PLC...");
 
    try
    {
        // 模拟指令列表:(寄存器地址, 写入值)
        List<(int Address, ushort Value)> commands = new List<(int, ushort)>
        {
            (40004, 1), // 启动指令
            (40005, 50),// 设定转速
            (40006, 25) // 设定温度
        };
 
        // 封装单条指令写入逻辑为Action
        Action<(int Address, ushort Value)> sendCommandAction = (cmd) =>
        {
            // 内部逻辑和读取类似:建立连接→写入寄存器→释放资源
            TcpClient tcpClient = null;
            IModbusMaster modbusMaster = null;
            try
            {
                tcpClient = new TcpClient(_plcIp, _plcPort);
                modbusMaster = ModbusIpMaster.CreateIp(tcpClient);
                modbusMaster.Transport.WriteTimeout = 2000;
 
                int offset = cmd.Address - 40001;
                // 写入单个寄存器(工控中下发指令的核心API)
                modbusMaster.WriteSingleRegister(1, offset, cmd.Value);
                Log($"指令下发完成:寄存器{cmd.Address} = {cmd.Value}");
            }
            finally
            {
                modbusMaster?.Dispose();
                tcpClient?.Close();
                tcpClient?.Dispose();
            }
        };
 
        // 并行执行所有指令(提升效率)
        List<Task> tasks = new List<Task>();
        foreach (var cmd in commands)
        {
            tasks.Add(Task.Run(() => sendCommandAction(cmd)));
        }
        await Task.WhenAll(tasks);
 
        Log("所有Modbus指令下发完成!");
    }
    catch (Exception ex)
    {
        Log($"下发指令失败:{ex.Message}");
    }
    finally
    {
        btnSendCommands.Enabled = true;
    }
}



关键说明:

  1. 1. 指令封装
    • • Action<(int Address, ushort Value)>:把单条写入指令封装成 Action,复用性强(比如不同设备的指令可以用同一个 Action);
    • • 工控中指令通常是 “地址 + 值” 的形式,比如 40004 写入 1 表示 “启动设备”,是 PLC 程序约定的逻辑。
  2. 2. **并行执行(Task.WhenAll)**:
    • • 批量下发指令时,并行执行比串行快数倍(比如 3 条指令串行要 3 秒,并行只要 1 秒);
    • • 适合工控中的 “广播指令”“批量参数配置” 场景,比如同时给多个寄存器设值。
  3. 3. WriteTimeout:写入超时必须设置,防止 PLC 忙时上位机无限等待。

五、场景 3:异步记录 Modbus 通信日志(非核心但必要)



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

private async void btnLog_Click(object sender, EventArgs e)
{
    Log("开始异步记录Modbus通信日志...");
 
    // 封装日志记录逻辑
    Func<Task> logLogic = async () =>
    {
        await Task.Delay(500); // 模拟写入文件/数据库耗时
        string logContent = $"Modbus通信日志:PLC IP={_plcIp},最后通信时间={DateTime.Now},状态=正常";
        Log($"日志已存储:{logContent}");
    };
 
    // 异步执行,捕获异常(火并忘但不丢异常)
    _ = logLogic().ContinueWith(task =>
    {
        if (task.Exception != null)
        {
            Log($"日志记录失败:{task.Exception.InnerException.Message}");
        }
    });
 
    Log("Modbus日志记录请求已提交(异步执行)!");
}



关键说明:

  • • **(Fire and Forget)**:日志记录是 “非核心业务”,不需要阻塞用户操作,所以用_ = logLogic()执行;
  • • ContinueWith:必须加 —— 防止日志写入失败时异常 “吞掉” 或导致程序崩溃,工控系统哪怕日志写失败,核心业务也不能停。

总结(核心要点回顾)

  1. 1. 异步核心:用async/await + Task.Run封装 Modbus 同步通信逻辑,避免 UI 卡死;Action/Func<Task>封装可复用的业务逻辑(日志、指令、读取);
  2. 2. Modbus 关键:地址偏移转换(40001→0)、资源释放(finally 块)、超时设置(Read/WriteTimeout)是通信成功的三大核心;
  3. 3. 工控规范:按钮禁用防重复操作、线程安全更新 UI、异常捕获提示、日志记录,是工控上位机稳定运行的必备设计。

这个示例完全复刻了实际工控项目的开发思路,你只要替换 PLC IP、寄存器地址和数据解析逻辑,就能直接用到现场项目中。


阅读原文:https://mp.weixin.qq.com/s/b91coPCOk56L3w1uPHjEBA


该文章在 2026/1/24 11:40:22 编辑过
关键字查询
相关文章
正在查询...
点晴ERP是一款针对中小制造业的专业生产管理软件系统,系统成熟度和易用性得到了国内大量中小企业的青睐。
点晴PMS码头管理系统主要针对港口码头集装箱与散货日常运作、调度、堆场、车队、财务费用、相关报表等业务管理,结合码头的业务特点,围绕调度、堆场作业而开发的。集技术的先进性、管理的有效性于一体,是物流码头及其他港口类企业的高效ERP管理信息系统。
点晴WMS仓储管理系统提供了货物产品管理,销售管理,采购管理,仓储管理,仓库管理,保质期管理,货位管理,库位管理,生产管理,WMS管理系统,标签打印,条形码,二维码管理,批号管理软件。
点晴免费OA是一款软件和通用服务都免费,不限功能、不限时间、不限用户的免费OA协同办公管理系统。
Copyright 2010-2026 ClickSun All Rights Reserved