События C # и безопасность streamов

ОБНОВИТЬ

Начиная с C # 6, ответ на этот вопрос:

SomeEvent?.Invoke(this, e); 

Я часто слышу / читаю следующий совет:

Всегда делайте копию события, прежде чем вы проверите его для null и уволите его. Это устранит потенциальную проблему с streamовой обработкой, когда событие станет null в месте, где вы проверяете значение null, и где вы запускаете событие:

 // Copy the event delegate before checking/calling EventHandler copy = TheEvent; if (copy != null) copy(this, EventArgs.Empty); // Call any handlers on the copied list 

Обновлено : я подумал, что, прочитав об оптимизации, это может также потребовать, чтобы член события был неустойчивым, но Джон Скит заявляет в своем ответе, что CLR не оптимизирует копию.

Между тем, для того, чтобы эта проблема даже возникла, другой stream должен был сделать что-то вроде этого:

 // Better delist from event - don't want our handler called from now on: otherObject.TheEvent -= OnTheEvent; // Good, now we can be certain that OnTheEvent will not run... 

Фактической последовательностью может быть эта смесь:

 // Copy the event delegate before checking/calling EventHandler copy = TheEvent; // Better delist from event - don't want our handler called from now on: otherObject.TheEvent -= OnTheEvent; // Good, now we can be certain that OnTheEvent will not run... if (copy != null) copy(this, EventArgs.Empty); // Call any handlers on the copied list 

Дело в том, что OnTheEvent работает после того, как автор OnTheEvent от подписки, и все же они просто OnTheEvent от подписки, чтобы избежать этого. Конечно, на самом деле нужна специальная реализация событий с соответствующей синхронизацией в add and remove accessors. Кроме того, существует проблема возможных взаимоблокировок, если блокировка сохраняется во время запуска события.

Так что это Cargo Cult Programming ? Похоже, что так много людей должны предпринять этот шаг, чтобы защитить свой код от нескольких streamов, когда на самом деле мне кажется, что события требуют гораздо большей осторожности, чем это, прежде чем они могут использоваться как часть многопоточного дизайна , Следовательно, люди, которые не берут на себя такую ​​дополнительную заботу, также могут игнорировать этот совет – это просто не проблема для однопоточных программ, и на самом деле, учитывая отсутствие volatile в большинстве онлайн-примеров кода, совет может иметь никакого эффекта вообще.

(И не проще ли просто назначить пустой delegate { } в объявлении участника, чтобы вам никогда не нужно было проверять null в первую очередь?)

Обновлено: В случае, если это было неясно, я понял намерение совета – избегать исключения нулевой ссылки при любых обстоятельствах. Моя точка зрения заключается в том, что это конкретное исключение ссылочной ссылки может возникнуть только в том случае, если другой stream исключает из события, и единственная причина для этого заключается в том, чтобы гарантировать, что никакие дальнейшие вызовы не будут получены через это событие, что явно НЕ достигается этой методикой , Вы будете скрывать состояние гонки – было бы лучше раскрыть это! Это исключительное исключение помогает обнаружить злоупотребление вашим компонентом. Если вы хотите, чтобы ваш компонент был защищен от злоупотреблений, вы можете следовать примеру WPF – сохранить идентификатор streamа в своем конструкторе и затем выбросить исключение, если другой stream попытается напрямую взаимодействовать с вашим компонентом. Или же реализовать действительно поточно-безопасный компонент (не простая задача).

Поэтому я утверждаю, что просто выполнение этой идиомы с копией / проверкой – это культовое программирование, добавляющее беспорядок и шум к вашему коду. Для эффективной защиты от других streamов требуется гораздо больше работы.

Обновление в ответ на сообщения блога Эрика Липперта:

Итак, есть большая вещь, которую я пропустил о обработчиках событий: «Обработчики событий должны быть надежными перед лицом вызова даже после того, как событие было отменено», и, очевидно, поэтому нам нужно только заботиться о возможности события делегат равен null . Это требование для обработчиков событий документировано где угодно?

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

Итак, остается один оставшийся fragment моего вопроса, почему явный-нуль-проверить «стандартный шаблон»? Альтернатива, назначающая пустой делегат, требует, чтобы в объявлении события добавлялось только = delegate {} , и это устраняет эти маленькие кучки вонючей церемонии из каждого места, где происходит событие. Было бы легко убедиться, что пустой делегат дешев для создания экземпляра. Или я все еще что-то пропущу?

Несомненно, это должно быть (как предложил Джон Скит), это просто совет .NET 1.x, который не вымер, как это должно было быть сделано в 2005 году?

Из-за условия JIT не разрешается выполнять оптимизацию, о которой вы говорите в первой части. Я знаю, что это было вызвано как призрак некоторое время назад, но это неверно. (Я проверил его с Джо Даффи или Вэнсом Моррисоном некоторое время назад, я не могу вспомнить, что.)

Без изменчивого модификатора возможно, что локальная копия будет устаревшей, но это все. Это не вызовет NullReferenceException .

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

 TheEvent(this, EventArgs.Empty); 

Предположим теперь, что список вызовов для этого делегата имеет 1000 записей. Вполне возможно, что действие в начале списка будет выполнено до того, как другой stream отменит подписчику в конце списка. Однако этот обработчик все равно будет выполнен, потому что это будет новый список. (Делегаты неизменны.) Насколько я вижу, это неизбежно.

Использование пустого делегата, безусловно, позволяет избежать проверки недействительности, но не фиксирует состояние гонки. Это также не гарантирует, что вы всегда «видите» последнее значение переменной.

Я вижу, что многие люди идут в сторону метода расширения этого …

 public static class Extensions { public static void Raise(this EventHandler handler, object sender, T args) where T : EventArgs { if (handler != null) handler(sender, args); } } 

Это дает вам лучший синтаксис для создания события …

 MyEvent.Raise( this, new MyEventArgs() ); 

А также удаляет локальную копию, так как она захватывается во время вызова метода.

«Почему явная-нуль-проверка« стандартного шаблона »?»

Я подозреваю, что причиной этого может быть то, что нулевой чек более эффективен.

Если вы всегда подписываете пустой делегат на свои события, когда они создаются, будут некоторые накладные расходы:

  • Стоимость создания пустого делегата.
  • Стоимость создания цепочки делегатов для его сдерживания.
  • Стоимость вызова бессмысленного делегата каждый раз, когда событие поднимается.

(Обратите внимание, что элементы управления пользовательского интерфейса часто имеют большое количество событий, большинство из которых никогда не подписываются. Чтобы создать фиктивный подписчик для каждого события, а затем вызвать его, вероятно, будет значительный успех.)

Я провел некоторое поверхностное тестирование производительности, чтобы увидеть влияние подхода subscribe-empty-delegate, и вот мои результаты:

 Executing 50000000 iterations . . . OnNonThreadSafeEvent took: 432ms OnClassicNullCheckedEvent took: 490ms OnPreInitializedEvent took: 614ms <-- Subscribing an empty delegate to each event . . . Executing 50000000 iterations . . . OnNonThreadSafeEvent took: 674ms OnClassicNullCheckedEvent took: 674ms OnPreInitializedEvent took: 2041ms <-- Subscribing another empty delegate to each event . . . Executing 50000000 iterations . . . OnNonThreadSafeEvent took: 2011ms OnClassicNullCheckedEvent took: 2061ms OnPreInitializedEvent took: 2246ms <-- Done 

Обратите внимание, что для случая с нулевым или одним абонентом (обычным для элементов управления пользовательского интерфейса, где события многочисленны), событие, предварительно инициализированное пустым делегатом, заметно медленнее (более 50 миллионов итераций ...)

Дополнительную информацию и исходный код можно найти в этом сообщении в блоге о безопасности streamов событий .NET Event, которые я опубликовал только за день до того, как этот вопрос был задан (!)

(Моя тестовая настройка может быть ошибочной, поэтому не стесняйтесь загружать исходный код и сами проверять его. Любая обратная связь очень ценится.)

Мне действительно понравилось это читать – нет! Хотя мне нужно, чтобы он работал с функцией C #, называемой событиями!

Почему бы не исправить это в компиляторе? Я знаю, что есть люди MS, которые читают эти сообщения, поэтому, пожалуйста, не пламените это!

1 – проблема с Null ). Почему бы не сделать события «.Empty» вместо «null» в первую очередь? Сколько строк кода будет сохранено для нулевой проверки или должно вставить объявление = delegate {} в объявление? Пусть компилятор обрабатывает пустой случай, IE ничего не делает! Если все это имеет значение для создателя события, они могут проверить .Empty и делать все, что им нужно! В противном случае все добавления null check / delegate – это хаки вокруг проблемы!

Честно говоря, я устал от того, чтобы делать это с каждым событием – как шаблонный код!

 public event Action Some; protected virtual void DoSomeEvent(string someValue) { var e = Some; // avoid race condition here! if(null != e) // avoid null condition here! e(this, someValue); } 

2 – проблема состояния гонки ). Я читаю сообщение в блоге Эрика, я согласен с тем, что обработчик H (обработчик) должен обрабатывать, когда он сам разыгрывает, но не может ли это событие быть неизменным / streamобезопасным? IE, установите флаг блокировки при его создании, так что всякий раз, когда он вызывается, он блокирует все подписки и не подписывается на него во время его выполнения?

Заключение ,

Разве современные языки не должны решать такие проблемы для нас?

Согласно Джеффри Рихтеру в книге CLR через C # , правильный метод:

 // Copy a reference to the delegate field now into a temporary field for thread safety EventHandler temp = Interlocked.CompareExchange(ref NewMail, null, null); // If any methods registered interest with our event, notify them if (temp != null) temp(this, e); 

Потому что он создает ссылочную копию. Для получения дополнительной информации см. Раздел «Событие» в книге.

Я использовал этот шаблон дизайна, чтобы гарантировать, что обработчики событий не будут выполнены после того, как они будут отписаны. До сих пор он работает очень хорошо, хотя я не пробовал профилирование производительности.

 private readonly object eventMutex = new object(); private event EventHandler _onEvent = null; public event EventHandler OnEvent { add { lock(eventMutex) { _onEvent += value; } } remove { lock(eventMutex) { _onEvent -= value; } } } private void HandleEvent(EventArgs args) { lock(eventMutex) { if (_onEvent != null) _onEvent(args); } } 

Я в основном работаю с Mono для Android в эти дни, и Android, похоже, не нравится, когда вы пытаетесь обновить представление после того, как его активность была отправлена ​​на задний план.

Эта практика заключается не в обеспечении соблюдения определенного порядка операций. Это на самом деле об исключении исключения с нулевой ссылкой.

Причины, по которым люди заботятся о нулевом контрольном исключении, а не о состоянии гонки, потребуют некоторых глубоких психологических исследований. Я думаю, что это связано с тем, что исправление нулевой ссылочной проблемы намного проще. Как только это будет исправлено, они повесят большой «Мисс Выполненный» баннер на свой код и разархивируют свой полетный костюм.

Примечание. Исправление состояния гонки, вероятно, связано с использованием синхронного флага, должен ли выполняться обработчик

Поэтому я немного опаздываю на вечеринку здесь. 🙂

Что касается использования нулевого, а не нулевого шаблона объекта для представления событий без подписчиков, рассмотрите этот сценарий. Вам нужно вызвать событие, но построение объекта (EventArgs) является нетривиальным, и в общем случае ваше событие не имеет подписчиков. Было бы полезно, если бы вы могли оптимизировать свой код, чтобы проверить, были ли у вас какие-либо подписчики, прежде чем вы приложили усилия по обработке аргументов и вызвали событие.

Имея это в виду, решение состоит в том, чтобы сказать «ну, нулевые подписчики представлены нулем». Затем просто выполните нулевую проверку перед выполнением дорогостоящей операции. Я полагаю, что другой способ сделать это состоял бы в том, чтобы иметь свойство Count в типе делегирования, поэтому вы выполнили бы дорогостоящую операцию, если myDelegate.Count> 0. Использование свойства Count – несколько приятный шаблон, который решает исходную проблему разрешающей оптимизации, а также обладает хорошим свойством быть вызванным без возникновения исключения NullReferenceException.

Однако имейте в виду, что, поскольку delegates являются ссылочными типами, им разрешено быть нулевыми. Возможно, нет простого способа скрыть этот факт под обложками и поддерживать только шаблон нулевого объекта для событий, поэтому альтернатива, возможно, заставила разработчиков проверить как нулевые, так и нулевые подписчики. Это было бы еще более уродливым, чем нынешняя ситуация.

Примечание. Это чистая спекуляция. Я не участвую в языках .NET или CLR.

С C # 6 и выше код можно упростить с помощью нового .? operator как в TheEvent?.Invoke(this, EventArgs.Empty);

https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/operators/null-conditional-operators

для однопоточных приложений вы согласны, что это не проблема.

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

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

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

Если вы не используете временную переменную и не используете пустой делегат, а кто-то отменил подписку, вы получите исключение с нулевой ссылкой, которое является фатальным, поэтому я считаю, что стоимость стоит того.

Я никогда не считал, что это большая проблема, потому что я обычно защищаю от такого рода потенциальной проблемы с streamами в статических методах (и т. Д.) Для моих повторно используемых компонентов, и я не делаю статические события.

Я делаю это неправильно?

Проводят все ваши мероприятия при строительстве и оставляют их в покое. Конструкция classа Delegate не может правильно обрабатывать любое другое использование, как я объясню в последнем абзаце этого сообщения.

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

Все, что может быть уведомлено, должно быть уведомлено. Если обработчики событий правильно обрабатывают уведомления (т. Е. Имеют доступ к авторитетному состоянию приложения и отвечают только тогда, когда это необходимо), тогда будет в порядке уведомлять их в любое время и доверять им, что они будут реагировать должным образом.

Единственный раз, когда обработчик не должен быть уведомлен о том, что произошло событие, – если событие на самом деле не произошло! Поэтому, если вы не хотите, чтобы обработчик был уведомлен, прекратите генерировать события (т. Е. Отключите элемент управления или все, что отвечает за обнаружение и появление события в первую очередь).

Честно говоря, я считаю, что class делегата является недостойным. Слияние / переход к MulticastDelegate было огромной ошибкой, потому что оно эффективно меняло (полезное) определение события из того, что происходит в один момент времени, к чему-то, что происходит в течение промежутка времени. Для такого изменения требуется механизм синхронизации, который может логически свернуть его обратно в один момент, но в MulticastDelegate отсутствует такой механизм. Синхронизация должна охватывать весь промежуток времени или мгновение, когда происходит событие, так что, как только приложение принимает синхронизированное решение для начала обработки события, оно полностью обрабатывает его (транзакционно). С черным ящиком, который является гибридным classом MulticastDelegate / Delegate, это почти невозможно, поэтому придерживайтесь использования одного абонента и / или реализуйте свой собственный вид MulticastDelegate, который имеет дескриптор синхронизации, который может быть выведен, пока цепочка обработчиков используется / модифицировано . Я рекомендую это, потому что альтернативой было бы избыточно реализовать синхронизацию / транзакционную целостность во всех ваших обработчиках, что было бы смешно / излишне сложным.

Please take a look here: http://www.danielfortunov.com/software/%24daniel_fortunovs_adventures_in_software_development/2009/04/23/net_event_invocation_thread_safety This is the correct solution and should always be used instead of all other workarounds.

“You can ensure that the internal invocation list always has at least one member by initializing it with a do-nothing anonymous method. Because no external party can have a reference to the anonymous method, no external party can remove the method, so the delegate will never be null” — Programming .NET Components, 2nd Edition, by Juval Löwy

 public static event EventHandler PreInitializedEvent = delegate { }; public static void OnPreInitializedEvent(EventArgs e) { // No check required - event will never be null because // we have subscribed an empty anonymous delegate which // can never be unsubscribed. (But causes some overhead.) PreInitializedEvent(null, e); } 

I don’t believe the question is constrained to the c# “event” type. Removing that restriction, why not re-invent the wheel a bit and do something along these lines?

Raise event thread safely – best practice

  • Ability to sub/unsubscribe from any thread while within a raise (race condition removed)
  • Operator overloads for += and -= at the class level.
  • Generic caller-defined delegate

Thanks for a useful discussion. I was working on this problem recently and made the following class which is a bit slower, but allows to avoid callings to disposed objects.

The main point here is that invocation list can be modified even event is raised.

 ///  /// Thread safe event invoker ///  public sealed class ThreadSafeEventInvoker { ///  /// Dictionary of delegates ///  readonly ConcurrentDictionary delegates = new ConcurrentDictionary(); ///  /// List of delegates to be called, we need it because it is relatevely easy to implement a loop with list /// modification inside of it ///  readonly LinkedList delegatesList = new LinkedList(); ///  /// locker for delegates list ///  private readonly ReaderWriterLockSlim listLocker = new ReaderWriterLockSlim(); ///  /// Add delegate to list ///  ///  public void Add(Delegate value) { var holder = new DelegateHolder(value); if (!delegates.TryAdd(value, holder)) return; listLocker.EnterWriteLock(); delegatesList.AddLast(holder); listLocker.ExitWriteLock(); } ///  /// Remove delegate from list ///  ///  public void Remove(Delegate value) { DelegateHolder holder; if (!delegates.TryRemove(value, out holder)) return; Monitor.Enter(holder); holder.IsDeleted = true; Monitor.Exit(holder); } ///  /// Raise an event ///  ///  public void Raise(params object[] args) { DelegateHolder holder = null; try { // get root element listLocker.EnterReadLock(); var cursor = delegatesList.First; listLocker.ExitReadLock(); while (cursor != null) { // get its value and a next node listLocker.EnterReadLock(); holder = cursor.Value; var next = cursor.Next; listLocker.ExitReadLock(); // lock holder and invoke if it is not removed Monitor.Enter(holder); if (!holder.IsDeleted) holder.Action.DynamicInvoke(args); else if (!holder.IsDeletedFromList) { listLocker.EnterWriteLock(); delegatesList.Remove(cursor); holder.IsDeletedFromList = true; listLocker.ExitWriteLock(); } Monitor.Exit(holder); cursor = next; } } catch { // clean up if (listLocker.IsReadLockHeld) listLocker.ExitReadLock(); if (listLocker.IsWriteLockHeld) listLocker.ExitWriteLock(); if (holder != null && Monitor.IsEntered(holder)) Monitor.Exit(holder); throw; } } ///  /// helper class ///  class DelegateHolder { ///  /// delegate to call ///  public Delegate Action { get; private set; } ///  /// flag shows if this delegate removed from list of calls ///  public bool IsDeleted { get; set; } ///  /// flag shows if this instance was removed from all lists ///  public bool IsDeletedFromList { get; set; } ///  /// Constuctor ///  ///  public DelegateHolder(Delegate d) { Action = d; } } } 

And the usage is:

  private readonly ThreadSafeEventInvoker someEventWrapper = new ThreadSafeEventInvoker(); public event Action SomeEvent { add { someEventWrapper.Add(value); } remove { someEventWrapper.Remove(value); } } public void RaiseSomeEvent() { someEventWrapper.Raise(); } 

Контрольная работа

I tested it in the following manner. I have a thread which creates and destroys objects like this:

 var objects = Enumerable.Range(0, 1000).Select(x => new Bar(foo)).ToList(); Thread.Sleep(10); objects.ForEach(x => x.Dispose()); 

In a Bar (a listener object) constructor I subscribe to SomeEvent (which is implemented as shown above) and unsubscribe in Dispose :

  public Bar(Foo foo) { this.foo = foo; foo.SomeEvent += Handler; } public void Handler() { if (disposed) Console.WriteLine("Handler is called after object was disposed!"); } public void Dispose() { foo.SomeEvent -= Handler; disposed = true; } 

Also I have couple of threads which raise event in a loop.

All these actions are performed simultaneously: many listeners are created and destroyed and event is being fired at the same time.

If there were a race conditions I should see a message in a console, but it is empty. But if I use clr events as usual I see it full of warning messages. So, I can conclude that it is possible to implement a thread safe events in c#.

Как вы думаете?

  • Когда используется пул streamов?
  • Должен ли я избегать обработчиков событий async void?
  • Событие Internet Explorer и JavaScript currentTarget
  • Выделить событие с текстовым полем?
  • Как проверить, была ли клавиша нажата клавишей со стрелкой в ​​Java KeyListener?
  • Реализация сканера штрих-кода на Java
  • BackgroundWorker RunWorkerCompleted Event
  • Как передать событие onClick для своего родителя на Android?
  • Как я могу отменить регистрацию «анонимного» обработчика событий
  • jquery Event.stopPropagation (), похоже, не работает
  • Добавление и удаление анонимного обработчика событий
  • Давайте будем гением компьютера.