如何防止在构造函数完成之前调用事件处理程序?

时间:2019-05-11 22:10:37

标签: c# thread-safety

如果我将事件挂接到构造函数中,那么在完成构造函数之前,该处理程序是否有可能被另一个线程调用?

例如:

private List<string> changes;

public MyClass(INotifyPropertyChanged observable) {
  observable.PropertyChanged += this.Handler;
  // Another thread changes a property at this point

  this.changes = new List<string>();
}

private void Handler(object sender, PropertyChangedEventArgs e) {
  this.changes.Add(e.PropertyName); // Breaks, because the list's not there yet
}

(是的,我知道在此示例中避免出现问题是微不足道的,我有一些比我想使其完全线程安全的情况更复杂的情况)

我可能只将lock(obj)放在事件处理程序和构造函数的主体之间,但这感觉很笨拙,我怀疑它可能容易以某种方式死锁。

是否有一种干净可靠的方法?

2 个答案:

答案 0 :(得分:1)

ECMA-335并没有强制CLI保证在构造函数完成之前,对构造函数所做的初始化更改应是可见的:

  

明确不要求CLI的一致性实现保证在构造函数完成之前,在构造函数内执行的所有状态更新是一致可见的(请参阅there,第 I.12.6.8 < / em>)。

因此,简短的答案是:避免在构造函数内部订阅实例事件处理程序,因为这意味着实例会暴露给外部使用者,而不能保证实例已准备好使用。

详细信息:构造函数的语义通常仅意味着将实例的内部数据转换为一致状态的状态初始化(当其所有不变量为真且可供其他对象使用时)。 C#中的事件机制本质上是对观察者模式的适应,这意味着它的参与者之间的交互数量和订阅的进行是这些交互之一,并且与实例中任何其他与其他对象的交互都应避免在实例为“ t保证要初始化。您正确地注意到了可能成为问题的可能情况,但是即使应用了诸如重新排序或同步之类的保护机制,也不能保证100%安全,因为CLI实施可能无法提供它,或者即使仍然存在构造器由于不依赖于构造器内部代码的原因而无法完成的场景,例如ThreadAbortException

当然,由于某些众所周知的约束(例如,您可以100%确保以不包含关键场景的方式实施事件发布者)可以对设计进行一些宽松的要求,但在一般情况下,我会建议在有单独方法的情况下将构建和订阅方案分开,这是公共合同的一部分,仅用于订阅。

答案 1 :(得分:0)

如何结合使用线程安全的集合(如ConcurrentQueue)和null conditional operator

  

线程安全的委托调用
  使用 ?。操作符,以检查委托是否为非null并以线程安全的方式(例如,引发事件时)调用该委托。

class MyClass
{
    private ConcurrentQueue<string> changes;

    public MyClass(INotifyPropertyChanged observable)
    {
        observable.PropertyChanged += this.Handler;
        // Another thread changes a property at this point

        this.changes = new ConcurrentQueue<string>();
    }

    private void Handler(object sender, PropertyChangedEventArgs e)
    {
        this.changes?.Enqueue(e.PropertyName);
        // Nothing breaks, changes during construction are simply not recorded
    }
}