如何解决避免反应模型中的竞争条件故障
我最近被一个要求使用“反应式编程”的 programming exercise 难住了。
问题陈述很简单:
- 使用级联值表达式实现“单元格”(很像 Excel)
- “输入单元格” - 具有静态值分配的单元格(即
Value = 1
) - “计算单元格” - 具有依赖单元格列表和用于计算值的 lambda 表达式的单元格(即
(array cells) => cells[0] - cell[1]
)
- “输入单元格” - 具有静态值分配的单元格(即
我的(有问题的)实现是这样的:
public abstract class Cell
{
protected int _value;
public virtual int Value
{
get {
return _value;
}
set {
if(_value != value)
{
_value = value;
OnChanged();
}
}
}
public event EventHandler<int> Changed;
public void OnChanged()
{
if(Changed is EventHandler<int> handler)
handler(this,Value);
}
}
public class InputCell : Cell
{
public InputCell() : this(0)
{
}
public InputCell(int value)
{
Value = value;
}
}
public class ComputeCell : Cell
{
public ComputeCell(IEnumerable<Cell> producers,Func<int[],int> compute)
{
_producers = new List<Cell>(producers);
_compute = compute;
_operands = new int[_producers.Count];
for(int i = 0; i < _operands.Length; i++)
{
var producer = _producers[i];
_operands[i] = producer.Value;
producer.Changed += new EventHandler<int>(Update);
}
}
private List<Cell> _producers;
private Func<int[],int> _compute;
private int[] _operands;
public override int Value { get { return _compute(_operands); } set {} }
private void Update(object sender,int newValue)
{
var was = Value;
for(int i = 0; i < _operands.Length; i++)
_operands[i] = _producers[i].Value;
if(was != Value)
OnChanged();
}
}
这对大量输入/测试用例“有效”,但最后一个测试用例失败:
var input = new InputCell(1);
var plusOne = new ComputeCell(new[] { input },inputs => inputs[0] + 1);
var minusOne = new ComputeCell(new[] { input },inputs => inputs[0] - 1);
var alwaysTwo = new ComputeCell(new[] { plusOne,minusOne },inputs => inputs[0] - inputs[1]);
// change value of dependent input cell
input.Value = 2
测试然后尝试断言 alwaysTwo
是否引发更改通知 - 不应该,因为输入值的更改对 alwaysTwo
的最终值没有影响.
但由于明显的竞争条件,我的实现引发了两个更改通知:
-
inputCell
--notifying-->plusOne
--notifying-->alwaysTwo
alwaysTwo
然后“认为”它的结果值已经改变,因为我们在等待第一个通知一路传播时阻止了对 minusTwo
的通知。
inputCell
/ \
/ \
/ \
v v
plusTwo minusTwo
\ /
\ /
\ /
v v
alwaysTwo
换句话说,我最终得到了这个菱形图,并且所有的北->南边路径总是在其他任何事情之前进行端到端的评估。
如何避免不必要地从 alwaysTwo
发出通知(即延迟到所有依赖单元都更新)?
wikipedia page for reactive programming 提到了这一类明显的问题,并建议在传播更改之前对依赖表达式进行拓扑排序,但我很难弄清楚如何/在何处应用它。
解决方法
这可以通过在更改之前使整个依赖树无效来解决。
我稍微简化了您的解决方案(优化了数据重复)并改进了该问题的解决方案。我还发现通过 EventArgs
传递新值没用。
public abstract class Cell
{
public event EventHandler Changing;
public event EventHandler Changed;
private int _value;
public int Value
{
get => _value;
protected set
{
if (_value != value)
{
OnChanging();
_value = value;
OnChanged();
}
}
}
public virtual bool IsValid
{
get => true;
protected set => throw new NotSupportedException();
}
public void OnChanging()
{
Changing?.Invoke(this,null);
}
public void OnChanged()
{
Console.WriteLine($"{this.GetType().Name} new Value {Value}");
Changed?.Invoke(this,null);
}
}
public class InputCell : Cell
{
public InputCell() : this(0) { }
public InputCell(int value)
{
Value = value;
}
public void SetValue(int value) => Value = value;
}
public class ComputeCell : Cell
{
private readonly List<Cell> _producers;
private readonly Func<int[],int> _compute;
public override bool IsValid { get; protected set; }
public ComputeCell(IEnumerable<Cell> producers,Func<int[],int> compute)
{
_producers = producers.ToList();
_compute = compute;
Compute();
foreach (Cell producer in _producers)
{
producer.Changed += Update;
producer.Changing += Invalidate;
}
}
private void Invalidate(object sender,EventArgs e)
{
IsValid = false;
}
private void Compute()
{
Value = _compute(_producers.Select(p => p.Value).ToArray());
}
private void Validate()
{
IsValid = _producers.All(p => p.IsValid);
}
private void Update(object sender,EventArgs e)
{
Validate();
if (IsValid)
Compute();
else
Console.WriteLine($"{this.GetType().Name} is invalid,can't change");
}
}
ComputeCell
无法更改其 Value
,同时至少有一个生产者处于无效状态(尚未重新计算)。
测试:
static void Main(string[] args)
{
Console.WriteLine("-1-");
var input = new InputCell(1);
Console.WriteLine("-2-");
var plusOne = new ComputeCell(new[] { input },inputs => inputs[0] + 1);
Console.WriteLine("-3-");
var minusOne = new ComputeCell(new[] { input },inputs => inputs[0] - 1);
Console.WriteLine("-4-");
var alwaysTwo = new ComputeCell(new[] { plusOne,minusOne },inputs => inputs[0] - inputs[1]);
Console.WriteLine($"alwaysTwo: {alwaysTwo.Value}");
input.SetValue(2);
Console.WriteLine($"alwaysTwo: {alwaysTwo.Value}");
}
输出:
-1-
InputCell new Value 1
-2-
ComputeCell new Value 2 // plusOne
-3- // minusOne remains at 0
-4-
ComputeCell new Value 2 // alwaysTwo
alwaysTwo: 2
InputCell new Value 2
ComputeCell new Value 3 // plusOne
ComputeCell is invalid,can't change // alwaysTwo has one of two cells in invalid state
ComputeCell new Value 1 // minusOne
// alwaysTwo remains at 2
alwaysTwo: 2
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。