『7x24小时有问必答』
1.png
目标是:UI不卡、通讯稳定、动作顺控可控、日志/数据不丢、异常可追踪。
1) 总体分层(标准形态)
通常拆成 6 个“长期运行”的工作域(每个域 1 个 Task/线程池任务):
UI线程(主线程)
只负责显示、按钮事件、绑定状态
不做任何阻塞 IO、不写业务流程
PLC/运动控制通讯循环线程
周期读写(例如 10ms~50ms)
维护连接、心跳、重连、数据镜像(Process Image)
设备驱动线程(相机/扫码枪/测量/IO板卡)
通常每类设备一个 worker(或统一设备管理器)
采集数据、触发抓拍、处理回调
顺控/工艺状态机线程(核心)
CASE Step 状态机
只读“数据镜像”,只写“命令意图”(Command)
不直接做阻塞通讯
报警与事件线程
报警判定、去抖、上报、弹窗节流
事件总线(EventBus)推送到 UI/日志/数据库
日志/数据库线程
异步落盘、异步入库
防止主流程被 IO 拖死
口诀:通讯有通讯循环、流程有状态机、IO有落盘线程,UI只显示。
2) 核心对象(让线程之间“有规矩地说话”)
(1) 数据镜像 ProcessImage(共享只读为主)
通讯线程每个周期刷新一次,把 PLC/驱动读到的数据写进来:
public  class  ProcessImage{       // 设备状态(建议用 struct/record + 原子替换)       public  PlcStatus Plc {  get;  init; }       public  AxisStatus[] Axes {  get;  init; } =  new  AxisStatus[4];       public  IoStatus Io {  get;  init; }       public  DateTime Timestamp {  get;  init; }}
共享策略建议:
写入端(通讯线程):构造新对象,然后 Interlocked.Exchange(ref _image, newImage)
读取端(状态机/UI/报警):只读 _image 的快照,避免加锁
(2) 命令队列 CommandQueue(只写意图,不直接写 PLC)
状态机线程、UI线程把“要做什么”丢进队列:
public  record  Command(string  Name,  object? Payload =  null);BlockingCollection<command></command> _cmdQueue =  new(new  ConcurrentQueue<command></command>());
通讯线程从队列取命令,翻译成 PLC 写入/驱动调用。
(3) 事件总线 EventBus(状态变化、报警、完成信号)
任何线程产生事件统一发布:
public  record  EventMsg(string  Type,  string  Text,  object? Data =  null);Channel<eventmsg> _eventCh = Channel.CreateUnbounded<eventmsg>();
3) 线程模型(推荐用 Task + CancellationToken)
启动骨架(标准写法)
CancellationTokenSource  cts  =  new();Task  commTask     =  Task.Run(() => CommLoop(cts.Token));Task  seqTask       =  Task.Run(() => SequenceLoop(cts.Token));Task  alarmTask    =  Task.Run(() => AlarmLoop(cts.Token));Task  logTask       =  Task.Run(() => LogLoop(cts.Token));Task  deviceTask  =  Task.Run(() => DeviceLoop(cts.Token));
停止:
先 cts.Cancel()
再 Task.WhenAll(...) 等待退出
确保队列/Channel Complete,避免卡死
4) 五个关键循环怎么写(工程里最稳的写法)
A) 通讯循环 CommLoop(固定周期 + 镜像刷新 + 执行命令)
职责:
维持连接(断线重连)
定周期读 PLC(以及从站/轴状态)
执行队列命令(写 PLC、发运动指令)
刷新 ProcessImage
要点:
固定周期:Stopwatch 控制节拍
超时保护、异常隔离:循环里 try/catch,失败计数触发重连
写入“脉冲命令”(如 Start/Home)要用一次性位(通讯层自动拉起→拉低)
伪代码:
void  CommLoop(CancellationToken ct){       var  sw = Stopwatch.StartNew();       long  periodMs =  20;       while  (!ct.IsCancellationRequested)      {             long  t0 = sw.ElapsedMilliseconds;            TryReconnectIfNeeded();             // 1) 读             var  plc = PlcReadAll();             var  axes = ReadAxes();             // 2) 执行命令(限制每周期处理条数,防止队列爆)             for  (int  i =  0; i <  20  && _cmdQueue.TryTake(out  var  cmd); i++)                  ExecuteCommandToPlc(cmd);             // 3) 写(把意图写到 PLC:例如输出位/目标位置/速度等)            PlcWriteOutputs();             // 4) 刷新镜像(原子替换)             var  newImage =  new  ProcessImage{ Plc = plc, Axes = axes, Timestamp = DateTime.Now };            Interlocked.Exchange(ref  _image, newImage);             // 5) 对齐周期             var  dt = sw.ElapsedMilliseconds - t0;             var  sleep = (int)(periodMs - dt);             if  (sleep >  0) Thread.Sleep(sleep);      }}
B) 顺控状态机 SequenceLoop(只看镜像 + 只发命令)
职责:
CASE Step(Idle/Init/Home/AutoRun/Fault…)
读 _image 判断条件
把“下一步动作”写成 Command 入队
关键原则:
顺控线程不要直接通讯/不要 sleep 很久
延时用“计时器变量”或 Stopwatch,不要阻塞整个系统
示例:
void  SequenceLoop(CancellationToken ct){       int  step =  0;       var  timer = Stopwatch.StartNew();       while  (!ct.IsCancellationRequested)      {             var  img = Volatile.Read(ref  _image);             switch  (step)            {                   case  0:  // Idle                         if  (img.Plc.StartPressed)                        {                              _cmdQueue.Add(new  Command("ServoOnAll"));                              step =  10;                              timer.Restart();                        }                         break;                   case  10:  // Wait ServoOn                         if  (img.Axes.All(a => a.ServoOn))                              step =  20;                         else  if  (timer.ElapsedMilliseconds >  3000)                              step =  900;  // Fault                         break;                   case  20:  // Home                        _cmdQueue.Add(new  Command("HomeAll"));                        step =  30;                        timer.Restart();                         break;                   case  30:                         if  (img.Axes.All(a => a.Homed))                              step =  100;  // Auto ready                         break;                   case  900:                         // fault handling                         break;            }            Thread.Sleep(5);  // 小睡避免空转      }}
C) 报警线程 AlarmLoop(去抖 + 分级 + 节流)
职责:
从镜像判定报警条件(急停、伺服报警、超时、气压不足)
去抖/延时确认
发布事件 _eventCh.Writer.TryWrite(...)
UI只订阅事件显示,不参与计算
D) 日志线程 LogLoop(所有线程只“投递”,日志线程落盘)
职责:
接收 EventMsg
写文本/写数据库
慢 IO 与实时控制解耦
E) 设备线程 DeviceLoop(相机/扫码枪等)
职责:
处理设备 SDK 回调,把结果作为事件/数据写入缓存
不要在 SDK 回调里做耗时操作(转投到队列/Channel)
5) 工业项目里最常踩的坑(避坑指南)
UI线程直接读写 PLC → UI卡死、偶发死锁
只让 CommLoop 做 IO
多个线程同时写 PLC → 写覆盖、脉冲丢失
所有写入统一从 CommLoop 出口
状态机里用 Thread.Sleep(1000) → 节拍乱、报警慢、响应差
用计时器/Stopwatch + step 条件推进
共享变量到处改 → 现场鬼畜 bug
镜像用“原子替换”,命令用队列
异常没集中处理 → 现场“偶发”难定位
每个 Loop 都 try/catch 并发 Event + 日志
6) 可以直接套用的“工程目录”
Core/ProcessImage.cs(镜像)
Core/Commands.cs(命令定义)
Core/EventBus.cs(事件)
Workers/CommWorker.cs
Workers/SequenceWorker.cs
Workers/AlarmWorker.cs
Workers/LogWorker.cs
Devices/CameraWorker.cs / ScannerWorker.cs
UI/ViewModels/*.cs(订阅事件、展示状态)

---

往期热门文章:
</eventmsg></eventmsg>

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

上一主题上一主题         下一主题下一主题
QQ手机版小黑屋粤ICP备17165530号

关于我们·投诉举报· 用户帮助· 联系我们 · 本站服务 · 版权声明· 隐私政策 · 投搞指南

法律保护:PLC技术网,plcjs.com,plcjs.net等字样
Copyright 2010-2030. All rights reserved. 


微信公众号二维码 抖音二维码 百家号二维码 今日头条二维码哔哩哔哩二维码