Chapter 7 外部意图
最理想的情况当然是把所有的处理都放到串行轮询这个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. 刷写底层} } } \]
- \(\boxed{\text{执行意图}}\):在LOOP每一轮开始前,先处理队列头部的前几个意图,直到队列已空或者达到单轮处理的最大意图数量。完成之后,如果缓存区已脏,则立刻刷写底层。
- \(\boxed{\text{读入数据}}\): 从底层硬件读取数据到缓存区。
- \(\boxed{\text{处理逻辑}}\): 串行执行逻辑组件。
- \(\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的手段。但是如果意图执行等待得不恰当,有可能会引发死锁。
考虑这样一个情况:
- (循环内部)某个Logicet中,捕获到相关业务A的请求信号(req)。该Logicet遂对外发布通知,要求处理。
- (循环外部)某处理器捕获到相同通知,执行完相关业务逻辑,最后写入意图,用以通知PLC开始另一个业务B。
- (循环内部)该Logicet await上一步完成,再向PLC反馈与业务A请求(req)相匹配的ack信号。
你能发现这里潜在的死锁吗?
重点看第3步:
- 该
Logicet在等待外部处理器执行完; - 而外部处理器向运行时写入了意图,然后干了什么?它在等待该意图被执行。
这里出现了环路等待的情况:Logicet在等外部处理器处理完;而外部处理器在等意图被执行完——但是,意图执行需要等到LOOP下一轮开始的时候才有可能被执行!
正确的做法有很多:
- 比如让外部处理不要直接写入意图——而是把修改
tags的动作改到循环内部。 - 或者,如果你一定要在外部写入意图,应该让内部不再死等外部结束:
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;
}
我个人更推荐尽量在Logicet内部进行信号处理,比起WriteIntent,Logicet结构清晰,而且执行时机固定、可预测。
还记得我们在逻辑组件中提到的注意点吗? 所有逻辑组件都是以预先设定的顺序串行轮询的,所以,在任何时候,都应该让
ProcessAsync(entry, thisChannel)这个方法尽早结束,否则,串行约束会让后续的逻辑组件长时间得不到执行。这里外部意图也有类似的要求——在任何时候,都应该让
TagGrpWriteIntent这个委托尽早结束,否则,串行约束会让后续的逻辑组件长时间得不到执行。
.IsEnabled对外部意图的影响
ITagGrp和ITagCbnt都提供了.IsEabled属性,用来在自动轮询中跳过一些群组和分组。
在外部意图中,如果是通过ITagGrp.WrirteAsync(ct)和ITagCbnt.WriteAsync(ct)方法来写入数据到底层,通常没什么问题;但是,如果通过setter修改属性.Value,则新值会等待到下一轮循环执行才会刷新到底层。