Как предотвратить вызов обработчика события до завершения работы конструктора? - PullRequest
2 голосов
/ 12 мая 2019

Если я перехватываю событие в конструкторе, есть ли вероятность, что обработчик может быть вызван другим потоком до его завершения конструктором?

например:

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 ]

1 голос
/ 12 мая 2019

ECMA-335 не обязывает CLI предоставлять гарантию того, что изменения инициализации, сделанные в конструкторе, должны быть видны до завершения конструктора:

Это явно не требование, чтобысоответствующая реализация CLI гарантирует, что все обновления состояния, выполняемые в конструкторе, будут равномерно видны до его завершения (см. там , раздел I.12.6.8 ).

Итак, краткий ответ: избегайте подписки обработчиков событий экземпляра внутри конструктора, поскольку это подразумевает передачу экземпляра внешним потребителям без гарантии того, что экземпляр готов к использованию.

Подробно: обычно семантика конструктора подразумевает только инициализацию состояния, которая переводит внутренние данные экземпляра в согласованное состояние (когда все его инварианты истинны и готовы к использованию другими объектами).Механизм событий в C # по сути является адаптацией паттерна наблюдателя, который подразумевает количество взаимодействий между его участниками, и создание подписки является одним из этих взаимодействий, и, как и любое другое взаимодействие с другим объектом, его следует избегать в конструкторе, когда экземпляр не существует.t гарантированно будет инициализирован.Вы правильно заметили возможный сценарий, когда он может стать проблемой, но даже с применением механизмов защиты, таких как переупорядочение или синхронизация, он не может быть гарантирован на 100% безопасным, поскольку он может быть не предоставлен реализацией CLI или даже если есть возможностьсценариев, когда конструктор не завершается по причине, не зависящей от кода внутри конструктора, например, из-за ThreadAbortException.

Конечно, могут быть некоторые смягченные требования к дизайну, продиктованные некоторыми известными ограничениями(например, вы можете быть на 100% уверены, что ваш издатель событий реализован способом, исключающим критические сценарии), но в общем случае я бы предложил разделить сценарии построения и подписки, когда есть отдельный метод, который является частью общедоступногодоговор, который предназначен только для подписки.

0 голосов
/ 12 мая 2019

Как насчет использования многопоточной коллекции (например, ConcurrentQueue) в сочетании с условным оператором null ?

Thread-безопасный вызов делегата
Используйте?.оператор, чтобы проверить, не является ли делегат ненулевым, и вызвать его потокобезопасным способом (например, когда вы вызываете событие).

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
    }
}
...