// 异步读PLC(有返回值)public async Task<bool> ReadPlcDataAsync(string ip, int address){ // await 后跟耗时操作:TCP通信/串口读写,不阻塞主线程 var client = new TcpClient(); await client.ConnectAsync(ip, 102); // 异步连接PLC,核心await // 后续读写逻辑... return true;}// 调用异步方法(3种正确方式)// 方式1:UI/主线程调用(推荐,不卡)private async void Btn_ReadPlc_Click(object sender, EventArgs e){ bool result = await ReadPlcDataAsync("192.168.0.1", 0);}// 方式2:后台任务调用Task.Run(async ()=> await ReadPlcDataAsync("192.168.0.1", 0));// 方式3:不等待(不推荐,丢结果)_ = ReadPlcDataAsync("192.168.0.1", 0);
二、Task vs ValueTask(工控选型关键)
核心区别:内存开销 + 适用场景,工控优先选 ValueTask(高频小任务)
Task:引用类型,存在堆上,有固定内存开销,适合低频、耗时久的任务(如 PLC 批量下载程序、大文件读写)
ValueTask<t>:值类型,栈上分配,开销极小,适合高频、耗时短的任务(如每秒读 1 次 PLC 寄存器、串口实时收数)选型口诀(工控直接套)
高频小任务(1 秒多次):用 ValueTask<t>
低频长任务(几秒 / 几分钟 1 次):用 Task<t>
避免:高频场景用 Task(堆内存频繁分配回收,工控程序长期运行会内存飙升)
实用代码对比
// 高频读PLC寄存器(用ValueTask,推荐)public async ValueTask<int> ReadPlcRegisterAsync(int regAddr){ // 模拟PLC寄存器快速读取(耗时短、高频调用) await Task.Delay(10); // 模拟通信延迟 return new Random().Next(0, 1000);}// 低频批量写PLC(用Task)public async Task WritePlcBatchAsync(List<int> addrList, List<int> valueList){ await Task.Delay(1000); // 模拟批量写入耗时 foreach(var i in addrList) { // 批量写逻辑 }}
三、异步流 IAsyncEnumerable(工控批量取数)
核心用途:异步分批返回数据,不用等所有数据加载完再返回,适合 PLC 批量读海量寄存器、日志分批读取,避免一次性加载占满内存关键语法:async + yield return + IAsyncEnumerable<t>,遍历用 await foreach工控实战(批量读 PLC 100 个寄存器,分批返回)
// 异步流:分批读PLC寄存器(避免一次性读100个卡主线程)public async IAsyncEnumerable<int> ReadPlcRegistersAsync(int startAddr, int count){ for (int i = 0; i < count; i++) { // 异步读单个寄存器(高频小任务用ValueTask) int value = await ReadPlcRegisterAsync(startAddr + i); yield return value; // 分批返回,读一个返一个 await Task.Delay(5); // 避免给PLC发请求太快 }}// 调用异步流(必须用await foreach)private async void Btn_BatchRead_Click(object sender, EventArgs e){ // 读PLC 0-99号寄存器,分批接收 await foreach (var regValue in ReadPlcRegistersAsync(0, 100)) { Console.WriteLine($"寄存器值:{regValue}"); // 实时刷新到UI界面(工控常用) txtLog.AppendText($"寄存器值:{regValue}\r\n"); }}
四、取消令牌 CancellationToken(工控必用,防卡死)
核心用途:安全取消异步任务,工控里用来处理:用户中途取消 PLC 读写、超时自动取消通信、程序退出时终止后台异步任务,避免任务卡死内存泄漏关键组件(2 个)
CancellationToken:令牌本身,传递给异步任务
CancellationTokenSource:令牌源,控制令牌的取消(调用 Cancel () 方法)2 个关键用法:手动取消、超时自动取消(工控全覆盖)
用法 1:手动取消(用户点击取消按钮)
private CancellationTokenSource _cts; // 全局令牌源(工控常用全局变量)// 开始异步读PLC(带取消令牌)private async void Btn_StartRead_Click(object sender, EventArgs e){ _cts = new CancellationTokenSource(); // 初始化令牌源 try { // 传递取消令牌到异步任务 await ReadPlcDataWithCancelAsync("192.168.0.1", 0, _cts.Token); } catch (OperationCanceledException) { txtLog.AppendText("PLC读取已取消\r\n"); // 取消异常,正常捕获 }}// 取消按钮(手动取消)private void Btn_CancelRead_Click(object sender, EventArgs e){ _cts?.Cancel(); // 触发取消 _cts?.Dispose(); // 释放资源(工控程序必须释放,避免内存泄漏)}// 带取消令牌的异步任务(核心)public async Task ReadPlcDataWithCancelAsync(string ip, int addr, CancellationToken token){ var client = new TcpClient(); // 给异步操作加取消令牌(关键:支持取消的异步方法都带token参数) await client.ConnectAsync(ip, 102, token); // 手动检测令牌(针对不支持token的异步操作,工控必加) while (true) { token.ThrowIfCancellationRequested(); // 检测到取消,抛异常终止任务 int value = await ReadPlcRegisterAsync(addr); txtLog.AppendText($"实时值:{value}\r\n"); await Task.Delay(100, token); }} 用法 2:超时自动取消(工控最常用,防通信卡死)
// PLC通信超时自动取消(10秒超时,工控标准配置)private async void Btn_TimeoutRead_Click(object sender, EventArgs e){ // 初始化令牌源,设置10秒超时(自动取消) using var cts = new CancellationTokenSource(10000); try { await ReadPlcDataWithCancelAsync("192.168.0.1", 0, cts.Token); } catch (OperationCanceledException) { txtLog.AppendText("PLC读取超时,自动取消\r\n"); }}
五、总结(必看)
不要在 async void 方法里吞异常(会导致程序崩溃),UI 事件外尽量用 Task/Task<t>
高频异步任务(如每秒读 PLC)必须用 ValueTask,禁用 Task(内存暴涨)
取消令牌必须手动释放(using 包裹或 Dispose),工控程序长期运行不释放会内存泄漏
</t></t></t></t></t></t></t>
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!