【问题标题】:Avoid race condition glitch in reactive model避免反应模型中的竞争条件故障
【发布时间】:2021-05-11 07:20:50
【问题描述】:

我最近被一个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 --通知--> plusOne --通知--> alwaysTwo

alwaysTwo 然后“认为”它的结果值已经改变,因为我们在等待第一个通知一直传播时阻止了对minusTwo 的通知。

      inputCell
        /   \
       /     \
      /       \
     v         v
  plusTwo   minusTwo
      \       /
       \     /
        \   /
         v v
      alwaysTwo

换句话说,我最终得到了这个菱形图,并且所有北->南边路径总是在其他任何事情之前进行端到端评估。

如何避免不必要地从alwaysTwo 发出通知(即延迟到所有相关单元格都更新后)?

wikipedia page for reactive programming 提到了这类明显的问题,并建议在传播更改之前对依赖表达式进行拓扑排序,但我很难弄清楚如何/在哪里应用它。

【问题讨论】:

    标签: c# event-handling reactive-programming


    【解决方案1】:

    这可以通过在更改之前使整个依赖树失效来解决。

    我在某种程度上简化了您的解决方案(优化了数据重复),并针对问题的解决方案进行了改进。我还发现通过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
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2015-01-30
      • 2010-09-25
      • 1970-01-01
      • 1970-01-01
      • 2010-09-25
      • 2019-06-12
      • 1970-01-01
      相关资源
      最近更新 更多