但是读数据的操作:发送指令,得到返回值。

如何不卡住主线程UI,又得读到数据?

这正是很多初学者最头疼的地方:既想要“同步”的结果(拿到数据),又想要“异步”的体验(不卡界面)。

在不使用复杂队列的情况下,最优雅的解决方案是利用 C# 的 async / await 异步模式。这就像是你在餐厅点餐:你扫码下单(发送指令)后可以继续玩手机(UI不卡),等菜好了服务员送过来(得到返回值)。

1. 终极轻量化方案:异步锁(SemaphoreSlim)

由于 lock 关键字不支持异步(不能在 lock 内部 await),我们需要把 object锁 换成 SemaphoreSlim(信号量)。它可以实现:同一时间只允许一个线程访问 PLC,且在等待期间彻底释放 UI 线程。C#

using System.Threading;
using System.Threading.Tasks;

public class AsyncPlcManager
{
    // 信号量:初始1个位子,最多1个位子。起到“异步锁”的作用。
    private readonly SemaphoreSlim _asyncLock = new SemaphoreSlim(1, 1);
    private IModbusMaster _master;
    private TcpClient _tcpClient;

    // 异步读方法:这是解决“不卡顿又能拿数据”的关键
    public async Task<ushort[]> ReadAsync(ushort addr, ushort len)
    {
        // 1. 等待进入权(如果别的线程在用,这里会异步等待,不卡UI)
        await _asyncLock.WaitAsync();
        
        try
        {
            if (_master == null) await ConnectAsync();
            
            // 2. 发送并等待 PLC 返回(异步读,不卡UI)
            return await _master.ReadHoldingRegistersAsync(1, addr, len);
        }
        catch (Exception ex)
        {
            Console.WriteLine($"读取失败: {ex.Message}");
            return null;
        }
        finally
        {
            // 3. 释放锁,让给下一个请求
            _asyncLock.Release();
        }
    }
}

2. 在窗体(UI)中如何调用?

在你的 Form 界面代码中,调用方式非常直观。只需要在按钮事件前加一个 async

C#

private async void btnRefresh_Click(object sender, EventArgs e)
{
    // 1. 界面显示“加载中...”
    lblStatus.Text = "正在读取...";
    
    // 2. 调用异步读取(UI 线程在这里会“暂时回家休息”,界面不会卡死)
    // 直到数据从 PLC 回来了,代码才会继续往下走
    ushort[] data = await _plcManager.ReadAsync(100, 5);
    
    // 3. 直接更新界面(此时已经在 UI 线程了,不需要 Invoke)
    if (data != null)
    {
        txtD100.Text = data[0].ToString();
        lblStatus.Text = "读取成功!";
    }
    else
    {
        lblStatus.Text = "读取失败!";
    }
}

3. 为什么这个方案比“队列”和“同步锁”都好?

特性 同步 lock 异步 SemaphoreSlim
界面表现 点击按钮后界面瞬间卡住,直到读取完成。 点击按钮后界面依然可以拖动,甚至能点别的按钮。
代码难度 简单,但体验差。 稍微高级一点,体验极佳。
数据安全性 安全(排队执行)。 安全(排队执行)
返回值获取 直接 Return。 使用 await 像拿同步返回值一样简单。

导出到 Google 表格

💡 核心总结

  • 不卡主线程的秘密:就在于 await。它告诉程序:“我现在要等 PLC 回话,你(UI线程)先去忙别的,等数据回来了再叫你回来执行后面的代码。”

  • 并发安全的保证:在于 SemaphoreSlim。它保证了即便用户疯狂点击按钮,所有的 ReadAsync 请求都会在门口乖乖排队,一个接一个地发给 PLC。

这种“异步锁”方案是不是既解决了代码太重的问题,又完美满足了你的 UI 需求? 如果你觉得 OK,我可以帮你整合出一个最终的、可直接跑在 .NET 4.8 上的完整类文件。

ushort[] data = await _plcManager.ReadAsync(100, 5);

假如一次要读取3秒才能成功,其他线程还在更新UI,或者还要手工点其他按钮,会不会卡住,点不了或界面动不了,不更新?

答案是:完全不会卡住! 这正是 await 的魅力所在。

即使这 3 秒钟内 PLC 迟迟不回话,你的界面依然可以自由拖动、点击其他按钮、输入文本框,甚至其他正在更新 UI 的定时器也会照常运行。

1. 这 3 秒钟里发生了什么?

当我们执行到 ushort[] data = await ... 时,程序内部发生了一个非常聪明的“暂停”:

  1. UI 线程释放:UI 线程在执行到 await 的瞬间,就被“释放”回去了。它立刻回到 Windows 的消息循环中,继续负责监听鼠标点击、重绘界面、处理定时器。

  2. 异步等待:网络底层(Socket)在另一个维度等待 PLC 的回复,这个过程不占用 UI 线程

  3. 断点续传:当 3 秒后数据终于从 PLC 回来了,系统会给 UI 线程发个通知:“喂,数据到了,你有空来把剩下的代码跑完”。

  4. 自动返回:UI 线程在处理完手头其他的点击事件后,会回到 await 之后的那一行代码,继续更新文本框。


2. 只有一种情况会让你觉得“卡”:重入(Re-entry)

界面虽然不卡,但会产生一个逻辑上的困惑: 如果在 3 秒的等待期内,用户又点了一次按钮,会发生什么?

  • 现象:如果你没做限制,用户点 10 下,就会有 10 个 ReadAsync 请求在信号量(SemaphoreSlim)门口排队。

  • 感受:用户会觉得“怎么没反应?”,然后 30 秒后(10次 × 3秒),界面开始疯狂刷新这 10 次的结果。

3. 如何优雅地处理这 3 秒?

为了给用户更好的体验,建议在调用时加一个简单的状态判断

private bool _isProcessing = false;

private async void btnRefresh_Click(object sender, EventArgs e)
{
    if (_isProcessing) return; // 如果正在读,点第二次就没反应,防止排队
    
    try 
    {
        _isProcessing = true;
        btnRefresh.Enabled = false; // 甚至可以把按钮禁用,变灰
        lblStatus.Text = "正在读取,请稍候 (最多3秒)...";

        // 执行异步读取(这3秒内,界面是活的,可以拖动,可以点别的按钮)
        ushort[] data = await _plcManager.ReadAsync(100, 5);

        if (data != null) txtResult.Text = data[0].ToString();
    }
    finally 
    {
        _isProcessing = false;
        btnRefresh.Enabled = true; // 恢复按钮
        lblStatus.Text = "就绪";
    }
}

4. 如果是“后台自动更新”线程呢?

如果你有一个定时器每秒都在读数据,而某次读取突然花了 3 秒:

  • 界面:依然不卡。

  • 读取:因为有 SemaphoreSlim 锁,下一秒的定时器请求会发现“锁还没开”,它会异步排队。

  • 表现:你会发现数据更新频率变慢了(变成 3 秒一更),但整个程序绝不会“假死”

💡 总结

使用 async / await + SemaphoreSlim

  • 不卡死:UI 线程是自由的。

  • 不撞车:PLC 请求是排队的。

  • 代码简洁:不用写复杂的 Thread 调度和 ConcurrentQueue。

这就是目前在 .NET 环境下处理 Modbus 通讯最推荐的“现代方案”。

你现在是打算在 WinForm 窗体里直接尝试这个 async 方法,还是需要我帮你把刚才那个 AsyncPlcManager 完善成一个可以直接粘贴使用的完整文件?

工具

快速

Gemini 是一款 AI 工具,其回答未必正确无误。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐