111,128
社区成员
发帖
与我相关
我的任务
分享要刷新PLC里的数据,被人告之用while(true),这是个死循环,会一遍又一遍地将程序从头到尾执行。
后来发现timer时钟,也可以每隔一段时间就循环执行它里面的程序,这样的话它也可以刷新PLC里的数据。
那么二者有什么不同?
先分清两件事:while(true) 是「主动轮询」,Timer 是「定时回调」。它们在「跑在哪条线程上、会不会阻塞」这两点上差别很大。
一、本质区别
while(true) 跑在你当前那条线程上,是同步阻塞循环。哪怕里面加了 Thread.Sleep,那也只是「占着这条线程睡觉」,线程仍然归它。
System.Timers.Timer / System.Threading.Timer 是把回调投递到线程池执行,调用方线程立刻返回,不复用你的 UI 线程。
二、放到上位机场景里,后果很具体
WinForms/WPF 只有一条 UI 线程。
写法 A(不推荐):
while (true)
{
ReadPlcData();
Thread.Sleep(500);
}
窗口会直接「未响应」。因为这条线程再也回不到消息循环,绘制、点击、重绘全部停摆。所以「程序一遍遍从头到尾执行」这个理解要修正一下:它是被同一条线程一遍遍占满,跟消息循环是两件事。
写法 B(Timer):
var timer = new System.Timers.Timer(500);
timer.Elapsed += (s, e) => ReadPlcData();
timer.AutoReset = true;
timer.Start();
UI 线程空闲,界面不卡,这才是刷新显示该用的形态。
三、但 Timer 有三个坑,选它之前必须知道
1)回调里不能直接碰控件。Elapsed 跑在线程池线程上,改 TextBox / DataGridView 必须走 Invoke / BeginInvoke,否则轻则跨线程异常,重则偶发闪退。
2)重入(overlap)。如果 ReadPlcData() 本身耗时超过 500ms,Timer 不会等你,它会再开一个回调,于是两个线程同时读同一个 PLC 连接,报文交错,读回来的是脏数据。这是 AutoReset = true 最典型的隐患。加个互斥:
private readonly SemaphoreSlim _gate = new(1, 1);
private async void OnElapsed(object s, ElapsedEventArgs e)
{
if (!await _gate.WaitAsync(0)) return;
try { await ReadPlcDataAsync(); }
finally { _gate.Release(); }
}
3)它不保证准点。500ms 的含义是「至少 500ms 后尝试执行」,线程池排队加上回调耗时,实际周期会漂移。要做趋势采样、算周期这类对时基敏感的事,不能把 Timer 当时间基准,得自己用 Stopwatch 记时刻。
四、那 while(true) 就一无是处吗
不是。它更适合放在后台专用线程里维护长连接:
_ = Task.Run(async () =>
{
while (!_cts.IsCancellationRequested)
{
try { await ReadPlcDataAsync(); }
catch (Exception ex) { Log(ex); }
await Task.Delay(500, _cts.Token);
}
});
这个写法看着也是 while(true),但它和 Timer 基本等价,因为 await Task.Delay 会真的让出线程,不是空转占 CPU。两者的差别只剩节拍口径:这个循环是「上一轮做完,再等 500ms」,实际周期 = 500ms + 读取耗时,天然串行、不会重入;Timer 是固定周期触发,所以才有上面的重入风险。
五、刷新 PLC 到底选哪个
只是定时刷新显示,用 Timer,简单且不卡 UI。
要求「上一轮必须做完才开始下一轮」的(绝大多数 PLC 轮询都属于这种),用后台 async 循环(Task.Run + await Task.Delay),省掉自己写重入保护。我自己在上位机项目里踩过这个坑:早期把轮询放在 UI 线程,PLC 一掉线,界面直接假死,最后只能杀进程。
另外两条比选型更要紧:别把 Thread.Sleep 放在 UI 线程;也别在循环里裸调用同步 Read。PLC 一旦掉线,同步读写可能阻塞几十秒,整个循环就卡死了。给每次读写加超时(Task.WhenAny(task, Task.Delay(timeout)) 是个便宜好用的写法),并在循环里做断线重连。
一句话收尾:两者真正的差别不在「能不能循环执行」,而在跑在哪条线程上、会不会阻塞、会不会重入。
所以呢,即使是
while(ture){
task.run(()=>{
// 你需要做的事情
})
await task.delay(1000)
}
效果其实和timer是一样的,因为这代码是个异步的。当然如果你这样写代码,需要注意的事情是“就是中间那个你需要做的事情,请保证他在1000内能做完”,不然你就等着去找那个博客园的一线码农去了。他最喜欢这样的了,这意味着他又可以水一篇“我有一个朋友”的已经水了5,6年的系列惊悚水文了
这个咋说呢,你的游戏,你的所有的桌面程序都有一个 while(true),这个叫消息循环体。你都提plc了,如果你去写plc代码,你会发现plc代码也就是一个while(true)
那么和你自己写while(true)区别在哪里呢? 区别在同步代码还是异步代码。
桌面程序 while(true) 消息循环,是用异步api接收系统消息泵里的消息,没消息他在逻辑上“堵住” 循环,实际上这个循环已经被释放了,他不占cpu
你自己写的,如果你写的是异步代码,那么这个while(true)将永远不释放,永远在使用cpu
至于timer把,有两种UI里的timer实际用的就是消息泵,他不占cpu,只是到时间了给UI发个消息,UI的那个while(ture)消息体收到后,在根据消息call 对应的方法。
实现的功能 其实都一样.. 比如 间隔3秒 从PLC读取数据 显示到winform上.
但是实现本质上 稍微有点区别.
如果你不使用线程 那么while+sleep 的时候会线程阻塞, 也就是UI看到的"卡死" 当然你肯定会新启动一个线程. 这样就无所谓了.
而timer不会阻塞线程,间隔达到后会触发事件 回调在线程池中执行.(效率比较高)
就算你while+线程 那么while会独占当前线程 不会被释放.
所以还是timer比较好
区别在于 自己做饭和点外卖
真用起来其实也没什么不同,while也可以设置间隔