Chapter 6 外部意图

最理想的情况当然是把所有的处理都放到串行轮询这个LOOP的中第二部分:

\[ \boxed{ \boxed{ \text{1. 读入数据} } \ \boxed{ \text{2. 处理逻辑} } \ \boxed{ \text{3. 刷写底层} } } \]

这样一来,各个ILogicet实例会以固定的顺序串行执行。

但事实上很多时候,我们需要从这个LOOP 的外部改变状态。在这种场景下,如果直接尝试写入,会带来非常大的不确定性——你甚至都不能确定对应的脏数据有没有被刷写到底层:也许在LOOP的某一轮结束后,你恰好手动更新了测点;但在同一时刻,下一轮正在从底层硬件读取缓存……这种并行不确定性会给程序员带来极大的不安。于是我引入了如下机制,以让来自外部的更改和自身的LOOP 串行执行:

\[ \boxed{ \boxed{ \text{0.执行意图} } \boxed{ \text{1. 读入数据} } \ \boxed{ \text{2. 处理逻辑} } \ \boxed{ \text{3. 刷写底层} } } \]

  1. \(\boxed{\text{执行意图}}\):在LOOP每一轮开始前,先处理队列头部的前几个意图,直到队列已空或者达到单轮处理的最大意图数量。完成之后,如果缓存区已脏,则立刻刷写底层。
  2. \(\boxed{\text{读入数据}}\): 从底层硬件读取数据到缓存区。
  3. \(\boxed{\text{处理逻辑}}\): 串行执行逻辑组件。
  4. \(\boxed{\text{刷写底层}}\): 刷写变更的点位到底层硬件。

意图入队

例如:

// 意图入队: 返回true 表示入队成功;如果入队失败,await t 将直接抛出异常
var res = proj.WriteIntent(
    "IoBox", 
    (entry, ct) => {
        ack.Value = true;
        return ValueTask.CompletedTask;
    }, 
    out var t
);

// 等待意图被执行完成
await t; 

注意,写意图本身只是将"意图"入队,out出的参数Task task并不会立刻被执行。意图一旦入队成功,则会在将来某个合适时间点被执行。你可以使用await task的手法,等待相关意图被执行完成,如果相关意图未能被正确执行,相关await会抛出异常。

意图执行和死锁陷阱

让我们重新审视这里的串行轮询机理:

\[ \boxed{ \boxed{ \text{0.执行意图} } \boxed{ \text{1. 读入数据} } \ \boxed{ \text{2. 处理逻辑} } \ \boxed{ \text{3. 刷写底层} } } \]

引入意图,是为了在原有的扫描循环之外,增加一种可以临时改变Tag的手段。但是如果意图执行等待得不恰当,有可能会引发死锁。

考虑这样一个情况:

  1. 某个Logicet中,捕获到相关业务A的请求信号(req)。该Logicet遂对外发布通知,要求处理。
  2. 外部处理器捕获到相同通知,执行完相关业务逻辑,最后写入意图,用以通知PLC开始另一个业务B。
  3. 等待上面都做完之后,该Logicet再向PLC反馈与业务A请求(req)相匹配的ack信号。

你能发现这里潜在的死锁吗?

重点看第3步:

  • Logicet在等待外部处理器执行完;
  • 而外部处理器向运行时写入了意图,然后干了什么?它在等待该意图被执行。

这里出现了环路等待的情况:Logicet在等外部处理器处理完;而外部处理器在等意图被执行完——但是,意图执行需要等到LOOP下一轮开始的时候才有可能被执行!

正确的做法是,让Logicet尽早结束:

if (req.GetTagValue<bool>() && !ack.GetTagValue<bool>() && Interlocked.CompareExchange(ref isBusy, 1, 0) == 0)
{

    // 如果外部处理器也需要写入意图并等待意图被执行完成,那么这里就不要等待,尽早结束
    _ = Task.Run(async () =>
    {
        // 这里的代码脱离了`Logicet`,所有的变更应当使用`WriteIntent`机制
        try
        {
            // 等待外部处理完成
            var evt = new ScrewResultEvt(block.CompleteBlock.DataBlock.ToResult());
            await _mediator.Publish(evt);

            // 写意图:确认
            var res = ctrl.Project!.WriteIntent(
                "IoBox", (entry, ct) =>
                {
                    ack.Value = true;
                    return ValueTask.CompletedTask;
                }, 
                out var t
            );
            await t;
        }
        catch (Exception ex)
        {
            // ... 错误处理
        }
        finally
        {
            var busy = Interlocked.CompareExchange(ref isBusy, 0, 1);
        }
    });
}
else if (!req.GetTagValue<bool>())
{
    ack.Value = false;
}

还记得我们在上一章逻辑组件中提到的注意点吗?

由于所有逻辑组件都是规定的顺序串行轮询的,所以,在任何时候,都应该让ProcessAsync(entry, thisChannel)这个方法尽早结束,否则,串行约束会让后续的逻辑组件长时间得不到执行。