多工站、多硬件工况下不加锁的风险分析

该代码涉及多个并发源:

  • 多个 IPLCBizHelper 实例可能在不同线程中触发事件(如扫码完成、上料完成、下料完成)。
  • Task.Run 启动的异步任务(EAP 交互、清料等)与主流程并行执行。
  • StartLot / EndLot 中并行启动多个业务对象的初始化/结批。
  • 事件回调中可能递归调用(如 OnCarriarArrive 内部调用自身)。

不加锁必然导致数据竞争和不一致。以下是具体风险点及示例:

1. ProductionSiteAbleQueue_MaxWell – 队列并发破坏

Dictionary<string, Queue<ProductionSite>> ProductionSiteAbleQueue_MaxWell
  • 写操作StartLot 中初始化 EnqueueOnCarriarArriveDequeueOnUnloadDone_MaxWellEnqueueOnLoadDone_MaxWell 中失败时 Enqueue
  • 并发场景:两个托盘同时到达不同设备,触发两个 OnCarriarArrive 并行执行,同时对一个设备队列 Dequeue,可能抛出 InvalidOperationException(队列空)或导致计数错误。
  • 不加锁后果:队列数据损坏,托盘分配错误,导致设备死锁或物料丢失。

2. ProducedCarrierSN – Dictionary 多线程写

Dictionary<string, int> ProducedCarrierSN
  • OnCommonEvent__PLC 的扫码处理分支中,当 TargetDevice == currentDeviceNo 时执行 Add
  • 多个 PLC 设备可能同时完成扫码,并行向同一个 Dictionary 添加不同键,未加锁会引发 ArgumentException(键已存在)或内部状态损坏。

3. DataRecordSNtoIndexMap – DataTable 非线程安全

  • OnLoadDone_MaxWell 中调用 DataRecord.Rows.Add() 并更新 SNtoIndexMap
  • EOT 中通过 DataRecord.Select() 查询并修改行数据。
  • 多个测试机同时完成测试,并发写入 DataTable 会导致 IndexOutOfRangeException 或数据混乱。

4. 布尔标志位 – 可见性与乱序执行

private bool m_CurrentLoadSignalDone = true;
private bool m_EndlotDoing = false;
private bool ClearProductionDone = true;
  • 多个线程读写这些标志(例如 OnCarriarArrive 中设置为 falseOnLoadDone_MaxWell 中设置为 true)。
  • 没有 volatile 或内存屏障,可能导致一个线程的修改对另一个线程不可见,从而出现重复上料或死等。

5. 集合 m_StartLotDoneList / m_EndLotDoneList

  • OnCommonEvent__EQPAddRun / EndLot 中遍历检查 Count
  • 并发 Add 可能破坏 List 内部数组,且 Count 检查不准确。

优化意见

一、强制使用线程安全集合或加锁保护

1. 队列改用 ConcurrentQueue + ConcurrentDictionary
private ConcurrentDictionary<string, ConcurrentQueue<ProductionSite>> ProductionSiteAbleQueue_MaxWell;
  • TryDequeue 原子操作,无需显式锁。
  • 但需注意:ConcurrentQueue 是无界的,对于有容量限制的缓存位需要额外控制。
2. ProducedCarrierSNConcurrentDictionary<string, int>
private ConcurrentDictionary<string, int> ProducedCarrierSN = new();
  • 使用 TryAdd 代替 Add,避免异常。
3. DataRecordSNtoIndexMap 加锁保护
private readonly object _dataLock = new object();
lock (_dataLock)
{
    DataRecord.Rows.Add(row);
    SNtoIndexMap[sn] = index;
}
// 同样在 EOT 中 lock 查询和更新
  • 或使用 ConcurrentBag 存储 DUT 快照,UI 更新时再转 DataTable。
4. 简单状态标志使用 Interlockedvolatile
private int _currentLoadSignalDone = 1; // 1=true, 0=false
Interlocked.Exchange(ref _currentLoadSignalDone, 0);
if (Interlocked.CompareExchange(ref _currentLoadSignalDone, 1, 0) == 0) ...
  • 或者使用 volatile bool 配合内存屏障(更简单但不适用于读-修改-写)。
5. 列表改用 ConcurrentBag 或加锁
private ConcurrentBag<string> m_StartLotDoneList = new();
// 或者 private readonly object _lotLock = new object();

二、避免在事件回调中执行阻塞/耗时操作

问题代码示例(OnCommonEvent__PLC 中直接弹窗):
Runtime.MainForm.Invoke(new Action(() =>
{
    m_LoadCarriarSNScanNGForm.ShowDialog();  // 阻塞等待用户输入
}));
  • 这会挂起 PLC 事件处理线程,导致后续信号无法及时响应,可能触发 PLC 超时。

优化

  • 将弹窗操作放到独立线程(Task.Run),回调完成立即返回,后续处理通过信号机制或再次触发事件完成。

三、消除递归调用和潜在死循环

OnCarriarArrive 结尾调用自身:
OnCarriarArrive(sender, null);
  • 如果队列为空或条件不满足,可能形成紧循环,消耗 CPU。
  • 应改为设置标志或使用定时器延迟重试,而非立即递归。
IsAllowProduceByCurrentDevice 中的 goto IsNextDeviceIsAllProcessing
  • 逻辑混乱,嵌套循环与标签降低可读性。建议重构为循环方法。

四、异步化长时间操作

OnCarriarArrive 中的 while 等待 BeforeSOT
while (ok != 0)
{
    // 循环重试,每次 Sleep(1000)
}
  • 阻塞 PLC 回调线程,影响整线响应。
  • 优化:异步轮询 + 状态机,或使用 TaskCompletionSource 等待设备就绪信号。

五、明确线程模型 – 建议使用单一事件处理管道

  • 当前各 PLC/EQP 事件直接在回调中执行业务逻辑,线程不可控。
  • 优化:引入一个队列(如 ConcurrentQueue<Action>)和单线程消费者,所有业务操作(上料、下料、状态变更)投递到此队列顺序执行。可极大简化并发控制。
  • 示例如下:
private BlockingCollection<Action> _workQueue = new();
private Task _workerTask;

void StartWorker() => _workerTask = Task.Run(() => {
    foreach (var action in _workQueue.GetConsumingEnumerable())
        action();
});

void Enqueue(Action action) => _workQueue.Add(action);
  • 所有事件回调只做 Enqueue(() => { 实际逻辑 })

六、其他建议

  1. 日志增强:在关键操作入口打印线程 ID,便于排查竞态。
  2. CancellationTokenSource 代替 while(状态) + Thread.Sleep
  3. EAP 回调 M_EAPHelper_OnMessageReceived 已经异步,但修改 ConcurrentDictionary 没问题,不过注意内部 Invoke 到 UI 线程可能死锁(应使用 BeginInvoke)。
  4. 参数解构Dictionary<string, Object> 频繁装箱拆箱,可定义强类型参数类。
  5. 状态机重构ProductionState 有多个状态,但切换时未检查合法性,可引入状态机类。

总结

不加锁绝对不可以。该代码在真实多工站并发场景下会频繁崩溃或产生逻辑错误。最低限度的修改是:

  • 将所有共享集合替换为 ConcurrentDictionary / ConcurrentQueue
  • DataTableList 等非安全集合加 lock
  • 将递归调用改为定时轮询或事件驱动。
  • 移除事件回调中的阻塞操作。

若要长期稳定运行,强烈建议重构为单线程事件循环模型,彻底消除并发复杂性。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐