PLC如何不卡住主线程UI,又得读到数据?读数据的操作:发送指令,得到返回值。
但是读数据的操作:发送指令,得到返回值。
如何不卡住主线程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 ... 时,程序内部发生了一个非常聪明的“暂停”:
-
UI 线程释放:UI 线程在执行到
await的瞬间,就被“释放”回去了。它立刻回到 Windows 的消息循环中,继续负责监听鼠标点击、重绘界面、处理定时器。 -
异步等待:网络底层(Socket)在另一个维度等待 PLC 的回复,这个过程不占用 UI 线程。
-
断点续传:当 3 秒后数据终于从 PLC 回来了,系统会给 UI 线程发个通知:“喂,数据到了,你有空来把剩下的代码跑完”。
-
自动返回: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 工具,其回答未必正确无误。
更多推荐


所有评论(0)