ОБНОВИТЬ
Начиная с C # 6, ответ на этот вопрос:
SomeEvent?.Invoke(this, e);
Я часто слышу / читаю следующие советы:
Всегда делайте копию события, прежде чем проверять его 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 не оптимизирует копию.
Но между тем, чтобы эта проблема даже возникла, другой поток должен был сделать что-то вроде этого:
// 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
работает после того, как автор отписался, и все же они просто отписались специально, чтобы избежать этого. Конечно , то , что действительно необходимо , так это реализация событий пользовательского с соответствующей синхронизацией в add
и remove
аксессорах. Кроме того, существует проблема возможных взаимоблокировок, если блокировка удерживается во время срабатывания события.
Так это программирование Cargo Cult ? Кажется, так - многие люди должны предпринять этот шаг, чтобы защитить свой код от нескольких потоков, когда на самом деле мне кажется, что события требуют гораздо большего внимания, чем это, прежде чем их можно будет использовать как часть многопоточного дизайна. , Следовательно, люди, которые не проявляют такой дополнительной заботы, могут также игнорировать этот совет - это просто не проблема для однопоточных программ, и на самом деле, учитывая отсутствие volatile
в большинстве примеров кода в Интернете, совет может не иметь эффект на всех.
(А не проще ли просто присвоить пустое delegate { }
в объявлении члена, чтобы вам никогда не приходилось проверять null
в первую очередь?)
Обновлено:В случае, если это не было ясно, я понял смысл совета - избегать исключений нулевой ссылки при любых обстоятельствах. Моя точка зрения заключается в том, что это исключение с нулевой ссылкой может возникнуть только в том случае, если другой поток исключен из события, и единственная причина для этого состоит в том, чтобы гарантировать, что никакие дополнительные вызовы не будут получены через это событие, что явно НЕ достигается этим методом , Вы бы скрывали состояние гонки - было бы лучше раскрыть это! Это нулевое исключение помогает обнаружить злоупотребление вашим компонентом. Если вы хотите, чтобы ваш компонент был защищен от злоупотреблений, вы можете последовать примеру WPF - сохранить идентификатор потока в конструкторе и затем выдать исключение, если другой поток пытается напрямую взаимодействовать с вашим компонентом. Или же реализовать действительно потокобезопасный компонент (задача не из легких).
Поэтому я утверждаю, что просто использование этой идиомы копирования / проверки является программированием культового груза, добавляющим беспорядок и шум в ваш код. На самом деле защита от других потоков требует гораздо больше работы.
Обновление в ответ на сообщения Эрика Липперта в блоге:
Итак, есть одна важная вещь, которую я пропустил в обработчиках событий: «обработчики событий должны быть устойчивыми перед вызовом даже после того, как событие было отписано», и, очевидно, поэтому нам нужно заботиться только о возможности события делегировать время null
. Задокументировано ли это требование к обработчикам событий?
И так: «Существуют и другие способы решения этой проблемы; например, инициализация обработчика для получения пустого действия, которое никогда не удаляется. Но выполнение нулевой проверки является стандартным шаблоном».
Итак, один оставшийся фрагмент моего вопроса, почему явно-нулевая проверка - «стандартный шаблон»? Альтернатива, назначение пустого делегата, требуется только = delegate {}
добавить в объявление события, и это устраняет эти маленькие груды вонючей церемонии из любого места, где происходит событие. Было бы легко убедиться, что пустой делегат дешевый для создания экземпляра. Или я все еще что-то упускаю?
Конечно, должно быть, что (как предположил Джон Скит) это всего лишь совет .NET 1.x, который не вымер, как это должно было быть в 2005 году?
источник
EventName(arguments)
безоговорочно вызывать делегат события, а не вызывать его только в случае ненулевого значения (ничего не делать в случае нулевого значения).Ответы:
JIT не разрешено выполнять оптимизацию, о которой вы говорите в первой части, из-за условия. Я знаю, что это было поднято как призрак некоторое время назад, но это не верно. (Я проверял это либо с Джо Даффи, либо с Вэнсом Моррисоном некоторое время назад; я не могу вспомнить, какой именно.)
Без модификатора volatile возможно, что локальная копия будет устаревшей, но это все. Это не вызовет
NullReferenceException
.И да, конечно, есть условия гонки - но так будет всегда. Предположим, мы просто изменили код на:
Теперь предположим, что список вызовов для этого делегата содержит 1000 записей. Вполне возможно, что действие в начале списка будет выполнено до того, как другой поток откажется от подписки обработчика в конце списка. Однако этот обработчик все равно будет выполнен, потому что это будет новый список. (Делегаты неизменны.) Насколько я вижу, это неизбежно.
Использование пустого делегата, безусловно, позволяет избежать проверки недействительности, но не устраняет условия гонки. Это также не гарантирует, что вы всегда «видите» последнее значение переменной.
источник
Я вижу, что многие люди идут к расширению метода сделать это ...
Это дает вам лучший синтаксис для поднятия события ...
А также устраняет локальную копию, поскольку она захватывается во время вызова метода.
источник
"Почему явно-нулевая проверка 'стандартного шаблона'?"
Я подозреваю, что причиной этого может быть то, что нулевая проверка более производительна.
Если вы всегда подписываете пустой делегат на ваши события при их создании, будут некоторые накладные расходы:
(Обратите внимание, что элементы управления пользовательского интерфейса часто имеют большое количество событий, на большинство из которых никогда не подписываются. Необходимость создать фиктивного подписчика на каждое событие и затем вызвать его, вероятно, будет значительным падением производительности.)
Я провел небольшое поверхностное тестирование производительности, чтобы увидеть влияние подхода подписки на пустой делегат, и вот мои результаты:
Обратите внимание, что в случае нуля или одного подписчика (обычно для элементов управления пользовательского интерфейса, где событий много), событие, предварительно инициализированное с пустым делегатом, заметно медленнее (более 50 миллионов итераций ...)
Дополнительную информацию и исходный код можно найти в этой записи блога, посвященной безопасности потоков вызовов .NET, которую я опубликовал всего за день до того, как был задан этот вопрос (!)
(Мои настройки теста могут быть ошибочными, поэтому не стесняйтесь загружать исходный код и проверять его самостоятельно. Любая обратная связь приветствуется.)
источник
Delegate.Combine
/Delegate.Remove
пару в случаях, когда у события есть ноль, один или два подписчика; если один раз добавлять и удалять один и тот же экземпляр делегата, различия в затратах между случаями будут особенно заметны, поскольку онCombine
имеет быстрое поведение в особом случае, когда один из аргументовnull
(просто возвращает другой), иRemove
очень быстро, когда два аргумента равно (просто вернуть ноль).Мне действительно понравилось это чтение - нет! Хотя мне это нужно для работы с функцией C #, которая называется events!
Почему бы не исправить это в компиляторе? Я знаю, что есть люди из MS, которые читают эти посты, так что, пожалуйста, не расстраивайтесь
1 - Нулевая проблема ) Почему бы вообще не сделать события пустыми вместо нулевых? Сколько строк кода будет сохранено для проверки на ноль или необходимости вставлять
= delegate {}
в объявление? Пусть компилятор обрабатывает пустой случай, IE ничего не делает! Если все это имеет значение для создателя события, они могут проверить .Empty и сделать все, что им захочется! В противном случае все пустые проверки / добавления делегатов - это хаки вокруг проблемы!Честно говоря, я устал от того, чтобы делать это с каждым событием - так называемый шаблонный код!
2 - проблема состояния гонки ) Я читаю сообщение Эрика в блоге, я согласен, что H (обработчик) должен обрабатывать, когда он разыменовывает себя, но нельзя ли сделать событие неизменным / безопасным для потока? IE, установите флаг блокировки при его создании, чтобы при каждом вызове он блокировал все подписки и отмены подписки на него во время его выполнения?
Вывод ,
Разве современные языки не должны решать такие проблемы для нас?
источник
С C # 6 и выше, код может быть упрощен с помощью
?.
оператора new, как в:TheEvent?.Invoke(this, EventArgs.Empty);
Вот документация MSDN.
источник
Согласно Джеффри Рихтеру в книге CLR через C # , правильный метод:
Потому что это заставляет справочную копию. Для получения дополнительной информации см. Его раздел «Событие» в книге.
источник
Interlocked.CompareExchange
потерпит неудачу, если ему каким-то образом будет передано значение nullref
, но это не то же самое, что передать aref
в место хранения (напримерNewMail
), которое существует и которое изначально содержит нулевую ссылку.Я использовал этот шаблон проектирования, чтобы гарантировать, что обработчики событий не будут выполнены после того, как они отписались. Пока он работает довольно хорошо, хотя я не пробовал профилирования производительности.
В настоящее время я в основном работаю с Mono для Android, и Android, похоже, не нравится, когда вы пытаетесь обновить представление после того, как его действие было отправлено в фоновый режим.
источник
Эта практика не о соблюдении определенного порядка операций. Это на самом деле о том, чтобы избежать исключения нулевой ссылки.
Рассуждения людей, которым небезразлично нулевое эталонное исключение, а не состояние гонки, потребуют глубоких психологических исследований. Я думаю, что это как-то связано с тем, что исправить проблему нулевых ссылок гораздо проще. Как только это будет исправлено, они вешают большой код «Миссия выполнена» на свой код и расстегивают молнию на своем летном костюме.
Примечание: исправление состояния гонки, вероятно, предполагает использование синхронного отслеживания флага, должен ли обработчик выполняться
источник
Unload
событию) безопасную отмену подписок объекта на другие события. Насти. Лучше просто сказать, что запросы на отмену подписки на события приведут к отмене подписки на события «в конце концов», и что подписчики событий должны проверять, когда они вызываются, есть ли что-нибудь полезное для них.Так что я немного опоздал на вечеринку здесь. :)
Что касается использования нулевого, а не нулевого шаблона объекта для представления событий без подписчиков, рассмотрим этот сценарий. Вам нужно вызвать событие, но создание объекта (EventArgs) нетривиально, и в общем случае ваше событие не имеет подписчиков. Было бы полезно, если бы вы могли оптимизировать свой код, чтобы проверить, есть ли у вас какие-либо подписчики, прежде чем вы предпримете обработку для построения аргументов и вызова события.
Учитывая это, решение состоит в том, чтобы сказать: «Ну, нулевые подписчики представлены нулем». Затем просто выполните нулевую проверку перед выполнением дорогостоящей операции. Я предполагаю, что другим способом сделать это было бы иметь свойство Count для типа Delegate, поэтому вы выполняете дорогостоящую операцию, только если myDelegate.Count> 0. Использование свойства Count - довольно приятный шаблон, который решает исходную проблему разрешить оптимизацию, и он также обладает хорошим свойством возможности вызываться, не вызывая исключение NullReferenceException.
Имейте в виду, однако, что, поскольку делегаты являются ссылочными типами, они могут быть нулевыми. Возможно, просто не было хорошего способа скрыть этот факт под прикрытием и поддерживать только шаблон нулевого объекта для событий, поэтому альтернатива могла заставить разработчиков проверять как нулевых, так и нулевых подписчиков. Это было бы даже хуже, чем нынешняя ситуация.
Примечание: это чистое предположение. Я не связан с языками .NET или CLR.
источник
+=
и-=
. Внутри класса допустимые операции также могут включать в себя вызов (со встроенной проверкой нуля), тестированиеnull
или настройку наnull
. Для чего-то еще нужно будет использовать делегата, именем которого будет имя события с некоторым конкретным префиксом или суффиксом.для однопоточных приложений вы исправляете это не проблема.
Однако, если вы создаете компонент, который предоставляет события, нет никакой гарантии, что потребитель вашего компонента не собирается использовать многопоточность, и в этом случае вам нужно подготовиться к худшему.
Использование пустого делегата решает проблему, но также вызывает снижение производительности при каждом вызове события и может иметь последствия для GC.
Вы правы в том, что пользователь пытается отменить подписку, чтобы это произошло, но если он прошел мимо временной копии, то рассмотрите сообщение, уже находящееся в пути.
Если вы не используете временную переменную и не используете пустой делегат, и кто-то отписывается, вы получаете исключение нулевой ссылки, которое является фатальным, поэтому я думаю, что цена того стоит.
источник
На самом деле я никогда не считал, что это большая проблема, потому что я обычно защищаю только от такого потенциального нарушения потоков в статических методах (и т. Д.) На моих повторно используемых компонентах, и я не создаю статические события.
Я делаю это неправильно?
источник
Проводите все ваши события на стройке и оставьте их в покое. Дизайн класса Delegate не может правильно обрабатывать любое другое использование, как я объясню в последнем параграфе этого поста.
Прежде всего, нет смысла пытаться перехватить уведомление о событии, когда ваши обработчики событий уже должны принять синхронизированное решение о том, следует ли / как реагировать на уведомление. .
Все, что может быть уведомлено, должно быть уведомлено. Если ваши обработчики событий обрабатывают уведомления надлежащим образом (то есть они имеют доступ к официальному состоянию приложения и отвечают только при необходимости), тогда можно будет уведомить их в любое время и поверить, что они ответят правильно.
Единственный раз, когда обработчик не должен быть уведомлен о том, что произошло событие, - это если событие фактически не произошло! Поэтому, если вы не хотите, чтобы обработчик уведомлялся, прекратите генерировать события (то есть отключите элемент управления или что-то еще, что в первую очередь отвечает за обнаружение и реализацию события).
Честно говоря, я думаю, что класс «Делегат» не спасен. Слияние / переход к MulticastDelegate было огромной ошибкой, потому что оно фактически изменило (полезное) определение события с того, что происходит в один момент времени, на то, что происходит за промежуток времени. Такое изменение требует механизма синхронизации, который может логически свернуть его обратно в одно мгновение, но MulticastDelegate не имеет такого механизма. Синхронизация должна охватывать весь промежуток времени или момент, когда происходит событие, поэтому, когда приложение принимает синхронизированное решение начать обработку события, оно завершает его обработку полностью (транзакционно). С черным ящиком, который является гибридным классом MulticastDelegate / Delegate, это почти невозможно, поэтомупридерживайтесь использования одного подписчика и / или реализуйте свой собственный тип MulticastDelegate, у которого есть дескриптор синхронизации, который может быть удален во время использования / изменения цепочки обработчиков . Я рекомендую это, потому что альтернативой может быть избыточная реализация синхронизации / целостности транзакций во всех ваших обработчиках, что было бы нелепо / излишне сложно.
источник
Пожалуйста, посмотрите здесь: http://www.danielfortunov.com/software/%24daniel_fortunovs_adventures_in_software_development/2009/04/23/net_event_invocation_thread_safety Это правильное решение и всегда должно использоваться вместо всех других обходных путей.
«Вы можете убедиться, что во внутреннем списке вызовов всегда есть хотя бы один член, инициализировав его анонимным методом« ничего не делать ». Поскольку ни одна внешняя сторона не может иметь ссылку на анонимный метод, никакая внешняя сторона не может удалить метод, поэтому делегат никогда не будет нулевым »- Программирование .NET Components, 2nd Edition, автор Juval Löwy
источник
Я не верю, что вопрос ограничен типом события c #. Сняв это ограничение, почему бы не заново изобрести колесо и не сделать что-то подобное?
Поднимите поток событий безопасно - лучшая практика
источник
Спасибо за полезное обсуждение. Недавно я работал над этой проблемой и создал следующий класс, который немного медленнее, но позволяет избежать вызовов удаленных объектов.
Суть в том, что список вызовов может быть изменен даже при возникновении события.
И использование это:
Тест
Я проверил это следующим образом. У меня есть поток, который создает и уничтожает объекты, как это:
В конструкторе
Bar
(объект слушателя) я подписываюсьSomeEvent
(что реализовано, как показано выше) и отписываюсь вDispose
:Также у меня есть пара потоков, которые вызывают событие в цикле.
Все эти действия выполняются одновременно: многие слушатели создаются и уничтожаются, а событие запускается одновременно.
Если бы были условия гонки, я бы увидел сообщение в консоли, но оно пустое. Но если я использую события clr как обычно, я вижу, что он полон предупреждающих сообщений. Итак, я могу заключить, что можно реализовать потокобезопасные события в c #.
Что вы думаете?
источник
disposed = true
может произойти раньшеfoo.SomeEvent -= Handler
в вашем тестовом приложении, что приведет к ложному срабатыванию. Но кроме этого есть несколько вещей, которые вы можете изменить. Вы действительно хотите использоватьtry ... finally
для замков - это поможет вам сделать это не только потокобезопасным, но и безопасным для прерывания. Не говоря уже о том, что вы могли бы избавиться от этого глупогоtry catch
. И вы не проверяете делегата, переданного вAdd
/Remove
- это может бытьnull
(вы должны сразу бросить вAdd
/Remove
).