C# 如何防止在构造函数之前调用事件处理程序';结束了吗?

C# 如何防止在构造函数之前调用事件处理程序';结束了吗?,c#,thread-safety,C#,Thread Safety,如果我在构造函数中钩住一个事件,处理程序是否有可能在完成构造函数之前被另一个线程调用 e、 g: 私有列表更改; 公共MyClass(INotifyPropertyChanged observable){ observable.PropertyChanged+=此.Handler; //另一个线程此时更改属性 this.changes=新列表(); } 私有void处理程序(对象发送方,PropertyChangedEventArgs e){ this.changes.Add(e.Propert

如果我在构造函数中钩住一个事件,处理程序是否有可能在完成构造函数之前被另一个线程调用

e、 g:

私有列表更改;
公共MyClass(INotifyPropertyChanged observable){
observable.PropertyChanged+=此.Handler;
//另一个线程此时更改属性
this.changes=新列表();
}
私有void处理程序(对象发送方,PropertyChangedEventArgs e){
this.changes.Add(e.PropertyName);//中断,因为列表还没有出现
}
(是的,我知道在这个例子中避免一个问题很简单,我有一些比这个更复杂的情况,我想让它们完全线程安全)

我可能只是在事件处理程序和构造函数主体周围放置了一个
锁(obj)
,但这感觉很笨拙,我怀疑它可能会以某种方式死锁


有没有一种干净可靠的方法可以做到这一点?

使用线程安全的集合(比如)和

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

class-MyClass
{
私有并发队列更改;
公共MyClass(INotifyPropertyChanged observable)
{
observable.PropertyChanged+=此.Handler;
//另一个线程此时更改属性
this.changes=新的ConcurrentQueue();
}
私有void处理程序(对象发送方,PropertyChangedEventArgs e)
{
this.changes?.Enqueue(e.PropertyName);
//在施工过程中,任何东西都不会断裂,任何变化都不会被记录下来
}
}

ECMA-335不要求CLI保证构造函数中所做的初始化更改在构造函数完成之前可见:

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

因此,简单的回答是:避免在构造函数中订阅实例事件处理程序,因为这意味着向外部使用者公开实例,而不保证实例已准备好使用

详细说明:通常,构造函数的语义只意味着将实例的内部数据置于一致状态的状态初始化(当它的所有不变量都为true并且准备好被其他对象使用时)。C#中的事件机制本质上是观察者模式的适应,这意味着参与者之间的交互数量,订阅是这些交互的一种,当实例不能保证初始化时,作为与其他对象的任何其他交互,应该在构造函数中避免。您正确地注意到了可能出现的问题,但即使应用了重新排序或同步等保护机制,也无法保证100%安全,因为它可能不是由CLI实现提供的,或者即使提供了,当构造函数由于不依赖于构造函数内部代码的原因(例如由于
ThreadAbortException
)而无法完成时,仍有可能出现这种情况


当然,在一些众所周知的约束条件下,对设计可能会有一些宽松的要求(例如,您可以100%确定您的事件发布器是以排除关键场景的方式实现的),但在一般情况下,我建议在有单独方法时,将构造场景和订阅场景分开,这是公共合同的一部分,仅用于订阅。

涉及多线程的问题通常通过解决该问题的特定解决方案来解决。如果你不了解自己的具体情况,就很难给出任何真正的、有用的答案。也就是说,发布的示例并不意味着构造函数/事件处理程序周围的锁(obj)块不起作用。除非您试图在构造函数和事件处理程序之间做一些巧妙的事情,否则死锁不应该是一个问题。一般来说,您不应该让
this
从构造函数中逃脱。您的设计违反了这一点。我认为
lock()
将是一个非常弱的解决方案。我更愿意重新排列你的陈述(我想这就是你所说的琐碎回避的意思)。这可能是微不足道的,但在没有死锁风险的情况下,它将是健壮的。是的,在这种情况下,重新排序将是微不足道的修复。我将尝试提出一个更好的例子,但我能想到的最明显的例子是传入一个
ObservableCollection
,您需要以某种方式处理每个项,以及处理传入的任何新项,这可能发生在您仍在构建时。这听起来像是一个您不应该遇到的问题。如果类未正确初始化,则不应添加事件侦听器。如果您尝试
private List changes=new List()这将解决问题,但这似乎是您不应该遇到的问题。
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
}
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
    }
}