这段代码中存在一个非常典型的“异步转同步”的阻塞等待模式(使用while循环和Thread.Sleep强行等待异步回调结果)。
为了让代码更优雅、可复用,并且避免Thread.Sleep阻塞主线程,我们可以将这个等待逻辑抽离成一个通用的、异步的(或同步的,根据你需求)公共方法。
这里提供两种设计方案:方案一(推荐的现代异步async/await方案,不阻塞线程)和方案二(保持原有逻辑的传统同步方案)。
方案一:通用异步等待方法(推荐 🚀)
在现代 C# 开发中,使用Task.Delay代替Thread.Sleep可以释放 CPU 线程,大幅提升系统并发性能(特别是在 PLC/MES 这种多托盘并发场景下)。
1. 提取出的公共等待方法
你可以将这个方法放到一个工具类(如WaitHelper)或当前类的私有/公共函数中:
usingSystem;usingSystem.Collections.Concurrent;usingSystem.Threading.Tasks;publicstaticclassWaitHelper{/// <summary>/// 通用的异步条件等待方法(带超时和自定义退出条件)/// </summary>/// <typeparam name="TKey">字典键类型</typeparam>/// <typeparam name="TValue">字典值类型</typeparam>/// <param name="cacheDic">存储结果的并发字典</param>/// <param name="key">要监控的键(如托盘号)</param>/// <param name="timeoutSeconds">超时时间(秒)</param>/// <param name="extraBreakCondition">外部中止条件(例如:状态变为IDLE时触发中断)</param>/// <param name="checkIntervalMs">检查轮询间隔(毫秒),默认500ms</param>/// <returns>返回获取到的结果,若超时或异常中断返回 null</returns>publicstaticasyncTask<TValue>WaitForResultAsync<TKey,TValue>(ConcurrentDictionary<TKey,TValue>cacheDic,TKeykey,inttimeoutSeconds,Func<bool>extraBreakCondition=null,intcheckIntervalMs=500)whereTValue:class{DateTimestartTime=DateTime.Now;// 当字典中包含这个键,且满足外部继续执行的条件时,进行循环while(cacheDic.ContainsKey(key)){// 检查外部状态是否要求中止(如:EAP被禁用、设备突然变更为IDLE等)if(extraBreakCondition!=null&&extraBreakCondition()){break;}// 检查字典里的值是否已经被填充(有结果了)if(cacheDic.TryGetValue(key,outTValuevalue)&&value!=null){// 成功获取到结果,从字典中移除并返回cacheDic.TryRemove(key,out_);returnvalue;}// 超时检查if((DateTime.Now-startTime).TotalSeconds>timeoutSeconds){break;// 触发超时,跳出循环}// 异步等待,不阻塞当前线程awaitTask.Delay(checkIntervalMs);}returnnull;// 超时或被外部条件中断,返回 null}}2. 重构后的原方法调用方式
使用上述异步方法后,你的原方法需要改为async void或async Task:
privateasyncvoidOnlineModeTrayScanProcessResult(SignalObjectsignal,IPLCBizHelperpLCBizHelper,stringcurrentLoadCarriarSN){// 1. 初始化字典this.TrayChecks.AddOrUpdate(currentLoadCarriarSN,t=>null,(k,v)=>null);// 2. 触发异步校验任务(不需要用 var task 变量接收)_=Task.Run(async()=>{varobj2=awaitthis.m_EAPHelper.CarriarCheckIn(currentLoadCarriarSN);// 注意:你原本的代码里拿到 obj2 后并没有赋值回 TrayChecks,// 实际业务中,此处应该把结果写入:this.TrayChecks[currentLoadCarriarSN] = obj2.Result...});// 默认结果varonlineCheckResult="1";inttimeoutSeconds=30;// 3. 调用公共异步等待方法// 将外部环境的状态作为 Lambda 表达式传入stringwaitResult=awaitWaitHelper.WaitForResultAsync(this.TrayChecks,currentLoadCarriarSN,timeoutSeconds,extraBreakCondition:()=>!this.m_EAP_Enable||this.ProductionState==ProductionState.IDLE||this.ProductionState==ProductionState.Ready);// 4. 判断是拿到结果还是超时了if(waitResult!=null){onlineCheckResult=waitResult;}else{// 如果字典里还存在这个键,说明是因为超时或状态改变退出的Log.Error($"托盘{currentLoadCarriarSN}EAP 校验超时或状态异常!");OnLog?.Invoke(this,ErrorLevel.Warning,$"托盘{currentLoadCarriarSN}EAP 校验超时或状态异常!");}// 5. 给自动化发处理结果if(Enum.TryParse(onlineCheckResult,outTrackInResultresult)){SendTrayScanProcessResult(signal,pLCBizHelper,currentLoadCarriarSN,result,Convert.ToInt32(pLCBizHelper.TargetDeviceNo));}}方案二:传统同步等待方法(保持 Thread.Sleep)
如果你不希望改动原方法的private void签名(不想引入async/await),可以使用这种纯同步阻塞的提取方式。
1. 提取出的公共同步方法
publicstaticclassWaitHelper{/// <summary>/// 通用的同步阻塞条件等待方法/// </summary>publicstaticTValueWaitForResult<TKey,TValue>(ConcurrentDictionary<TKey,TValue>cacheDic,TKeykey,inttimeoutSeconds,Func<bool>extraBreakCondition=null,intcheckIntervalMs=500)whereTValue:class{DateTimestartTime=DateTime.Now;while(cacheDic.ContainsKey(key)){if(extraBreakCondition!=null&&extraBreakCondition()){break;}if(cacheDic.TryGetValue(key,outTValuevalue)&&value!=null){cacheDic.TryRemove(key,out_);returnvalue;}if((DateTime.Now-startTime).TotalSeconds>timeoutSeconds){break;}Thread.Sleep(checkIntervalMs);// 阻塞当前线程}returnnull;}}2. 原方法调用方式
privatevoidOnlineModeTrayScanProcessResult(SignalObjectsignal,IPLCBizHelperpLCBizHelper,stringcurrentLoadCarriarSN){this.TrayChecks.AddOrUpdate(currentLoadCarriarSN,t=>null,(k,v)=>null);Task.Run(async()=>{varobj2=awaitthis.m_EAPHelper.CarriarCheckIn(currentLoadCarriarSN);});varonlineCheckResult="1";inttimeoutSeconds=30;// 同步调用,直接阻塞等待stringwaitResult=WaitHelper.WaitForResult(this.TrayChecks,currentLoadCarriarSN,timeoutSeconds,extraBreakCondition:()=>!this.m_EAP_Enable||this.ProductionState==ProductionState.IDLE||this.ProductionState==ProductionState.Ready);if(waitResult!=null){onlineCheckResult=waitResult;}else{Log.Error($"托盘{currentLoadCarriarSN}EAP 校验超时!");OnLog?.Invoke(this,ErrorLevel.Warning,$"托盘{currentLoadCarriarSN}EAP 校验超时!");}if(Enum.TryParse(onlineCheckResult,outTrackInResultresult)){SendTrayScanProcessResult(signal,pLCBizHelper,currentLoadCarriarSN,result,Convert.ToInt32(pLCBizHelper.TargetDeviceNo));}}💡 代码优化小建议(避坑)
你在Task.Run中调用了await this.m_EAPHelper.CarriarCheckIn(currentLoadCarriarSN),但是拿到的obj2并没有写回到this.TrayChecks字典中。
正常情况下,MES/EAP 收到结果后,应该在某个地方执行:
this.TrayChecks[currentLoadCarriarSN]="某个结果码(如0或1)";如果CarriarCheckIn本身不会自动去更新TrayChecks,记得在Task.Run里面加上赋值逻辑,否则这个等待方法会百分之百触发超时。