针对EQPBizHelper类的并发分析及优化意见
用户提供了EQPBizHelper类的完整实现,该类代表一个测试工站,与 PLC 控制器、上位机 UI、MES/EAP 等模块交互。在多工站、多硬件环境下,该类的多个方法会被不同线程调用(如硬件消息接收线程、定时模拟线程、外部业务驱动线程)。不加锁是绝对不可行的,当前代码中存在多处严重的数据竞争隐患,可能导致程序崩溃、数据错乱或死锁。
一、共享资源与并发风险点
1.SN2Duts(Dictionary<string, DUT>)
privateDictionary<string,DUT>SN2Duts- 写入:
SOT方法中SN2Duts[dutsn] = dut; - 读取/删除:
EOT方法中SN2Duts.ContainsKey(dutSn)、SN2Duts.Remove(dutSn) - 并发场景:多个托盘同时在不同工站完成测试,可能同时调用
EOT;或者一个工件正在SOT添加时,另一个工件EOT尝试删除同一个键(虽然不同 SN 一般不会冲突,但 Dictionary 内部结构在多线程同时 Add/Remove 时会被破坏,引发ArgumentException或IndexOutOfRangeException)。 - 风险:高并发下程序崩溃。
2.WaitProducingDUTs(Queue) 和ProducedDUTs(Queue)
- 写入:
SOT中不直接操作,但模拟线程SimlateProductionLoop中Dequeue;EOT中ProducedDUTs.Enqueue(dut) - 并发:
SimlateProductionLoop线程与EOT同时操作ProducedDUTs;WaitProducingDUTs在模拟线程中Dequeue,但外部没有Enqueue(可能在其他地方),仍存在多线程读写。 - 风险:
Queue非线程安全,并发Enqueue/Dequeue导致数据损坏或异常。
3.EOTmapQueue(Queue<Dictionary<string,object>>)
privateQueue<Dictionary<string,object>>EOTmapQueue=newQueue<Dictionary<string,object>>();- 写入:
MessageReceived中EOTmapQueue.Enqueue(map) - 读取:
MessageReceived中EQPEOUSend分支EOTmapQueue.Dequeue() - 并发:两个事件都发生在硬件消息接收线程中,但可能先后触发,由于
Queue非线程安全,在高频消息下可能导致枚举失败或计数错误。更危险的是,EOT方法也可能通过其他路径调用(如直接调用),但当前代码中EOT并不直接操作该队列。
4.CurrentUnloadSNMap(Dictionary<string, bool>)
- 写入:
EOT中CurrentUnloadSNMap[dutSn] = false - 读取:
UnloadSNCheck中CurrentUnloadSNMap.ContainsKey(sn) - 并发:
EOT可能在多线程中同时执行,多个工件完成测试时同时向该字典添加条目,造成内部状态损坏。
5.NowDoingCnt(int)
- 读写:
SOT中NowDoingCnt++;EOT中NowDoingCnt--;BeforeSOT中读取NowDoingCnt判断 - 并发:无锁、无
volatile,导致自增/自减操作不是原子的,可见性无法保证。可能产生计数错误(如同时++两次只增加一次)。
6.IsProcessing/HasCacheSpace(bool)
- 写入:
SOT和EOT中根据NowDoingCnt设置这两个标志 - 读取:
AutoBizDriver中的IsAllowProduceByCurrentDevice会读取这些标志 - 并发:与
NowDoingCnt类似,缺少同步,可能读到过期值,导致上料决策错误。
7.m_BeforeSotDoing(bool)
- 写入:
BeforeSOT中先置true,最后置false - 读取:
BeforeSOT开头检查if (m_BeforeSotDoing) return -1; - 并发:虽意在防止重入,但无内存屏障,两个线程可能同时看到
false并同时进入,导致重复执行。
8.SN2Site(ConcurrentDictionary)
- 已使用
ConcurrentDictionary,线程安全,但注意Clear()在EOT末尾调用,可能与其他线程的TryAdd同时进行(风险较小,但仍需注意逻辑一致性)。
二、其他隐式并发问题
1.模拟线程SimlateProductionLoop与真实硬件线程的混用
- 当
SimulateMode = true时,该线程会访问WaitProducingDUTs、ProducingDUTs、SN2Site,同时SOT、EOT也会被调用(模拟模式下也会走SOT、EOT逻辑)。两者之间没有同步,极易冲突。
2.事件回调中的跨线程 UI 操作
OnLog、OnException等事件可能在任意线程触发,而订阅者可能是 UI 线程。当前代码未使用Control.Invoke/BeginInvoke,可能导致 UI 异常。
3.DataWrite中操作DataStructTemplateTable
DataStructTemplateTable是一个DataTable,在SOT和EOT中可能被读写(SOT中创建,EOT中通过DataWrite添加行)。DataTable非线程安全,多工件并发测试会导致内部状态损坏。
三、优化方案
1.全面使用线程安全集合
| 现有类型 | 推荐类型 | 说明 |
|---|---|---|
Dictionary<string, DUT> | ConcurrentDictionary<string, DUT> | 使用TryAdd、TryRemove、ContainsKey |
Queue<DUT> | ConcurrentQueue<DUT> | 使用Enqueue、TryDequeue |
Queue<object> | ConcurrentQueue<object> | 同上 |
Queue<Dictionary<string,object>> | ConcurrentQueue<Dictionary<string,object>> | 同上 |
Dictionary<string, bool> | ConcurrentDictionary<string, bool> | 使用TryAdd、ContainsKey |
2.原子操作与内存屏障
- 对于简单计数器
NowDoingCnt,使用Interlocked.Increment/Interlocked.Decrement。 - 对于布尔标志(如
m_BeforeSotDoing),使用Interlocked.Exchange或声明为volatile并结合lock语句确保原子性。建议使用lock保护整个BeforeSOT入口。
privatereadonlyobject_beforeSotLock=newobject();publicintBeforeSOT(objectobj){lock(_beforeSotLock){if(m_BeforeSotDoing)return-1;m_BeforeSotDoing=true;// ...m_BeforeSotDoing=false;return0;}}3.保护DataStructTemplateTable和DataWrite操作
- 对
DataTable的访问(添加列、添加行)加lock。
privatereadonlyobject_dataTableLock=newobject();lock(_dataTableLock){if(!DataStructTemplateTable.Columns.Contains(paraName))DataStructTemplateTable.Columns.Add(paraName);// 添加行等操作}4.消除模拟线程与真实线程的共享冲突
- 若
SimulateMode为 true,应完全隔离真实硬件调用。推荐:在SimulateMode下,禁止任何外部调用SOT/EOT(通过标志控制),所有模拟逻辑仅由SimlateProductionLoop内部完成。
5.事件回调中确保 UI 安全
- 在所有
OnLog、OnException调用处,如果需要在 UI 上显示,订阅方应自行处理Invoke。但更好的做法是:在EQPBizHelper内部不假设 UI 存在,仅通过事件抛出,由上层负责线程封送。
6.使用CancellationTokenSource替代while(SimulateMode)中的无限循环
- 便于优雅退出。
7.减少共享状态,增加不可变设计
- 例如
EOTmapQueue中存储的Dictionary对象在入队后不应再被修改。当前代码中入队后立即返回,没有修改风险,但Dequeue后直接传给EOT方法,EOT内部会读取字典内容,这是安全的。
8.对于IsProcessing和HasCacheSpace,由于它们依赖于NowDoingCnt,可以通过计算属性实时获取,而非存储状态
publicboolIsProcessing=>NowDoingCnt>0;publicboolHasCacheSpace=>NowDoingCnt<2;// 根据实际逻辑这样避免了状态不一致。
四、典型竞态示例及修正代码
原代码(有竞态):
NowDoingCnt++;if(NowDoingCnt>0)IsProcessing=true;if(NowDoingCnt<2)HasCacheSpace=true;修正后(原子 + 无状态标志):
intnewCount=Interlocked.Increment(refNowDoingCnt);// 不再单独维护 IsProcessing/HasCacheSpace,改用属性实时计算原代码(Dictionary 并发):
SN2Duts[dutsn]=dut;// 可能与其他线程的 Remove 冲突修正后:
if(!_sn2Duts.TryAdd(dutsn,dut)){Log.Warn($"Duplicate SN{dutsn}added to SN2Duts");}五、总结
不加锁在高并发、多工站环境下必然导致数据损坏和逻辑错误。建议立即实施以下最低限度修改:
- 将所有普通
Dictionary/Queue替换为ConcurrentDictionary/ConcurrentQueue。 - 对
NowDoingCnt使用Interlocked操作。 - 对
BeforeSOT的入口加lock防止重入。 - 对
DataStructTemplateTable的所有访问加lock。 - 移除
IsProcessing/HasCacheSpace字段,改用基于NowDoingCnt的计算属性。 - 在模拟模式下,禁止外部调用
SOT/EOT,或者将模拟线程与真实逻辑完全分离。
经过上述优化后,系统可在多工站并行加工时保持稳定,不会因并发导致崩溃。若需要更高性能,可进一步设计无锁数据结构,但当前场景下使用Concurrent集合已足够。